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, 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
16 if sys.platform.startswith('linux'):
20 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')
30 if not sys.platform.startswith('cygwin'):
34 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
37 from bup._helpers import get_linux_file_attr, set_linux_file_attr
39 # No need for a warning here; the only reason they won't exist is that we're
40 # not on Linux, in which case files don't have any linux attrs anyway, so
41 # lacking the functions isn't a problem.
42 get_linux_file_attr = set_linux_file_attr = None
45 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
47 # Q: Consider hardlink support?
48 # Q: Is it OK to store raw linux attr (chattr) flags?
49 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
50 # Q: Is the application of posix1e has_extended() correct?
51 # Q: Is one global --numeric-ids argument sufficient?
52 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
53 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
55 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
56 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
57 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
58 # FIXME: Consider pack('vvvvsss', ...) optimization.
59 # FIXME: Consider caching users/groups.
63 # osx (varies between hfs and hfs+):
64 # type - regular dir char block fifo socket ...
65 # perms - rwxrwxrwxsgt
66 # times - ctime atime mtime
69 # hard-link-info (hfs+ only)
72 # attributes-osx see chflags
78 # type - regular dir ...
79 # times - creation, modification, posix change, access
82 # attributes - see attrib
84 # forks (alternate data streams)
88 # type - regular dir ...
89 # perms - rwxrwxrwx (maybe - see wikipedia)
90 # times - creation, modification, access
91 # attributes - see attrib
95 _have_lchmod = hasattr(os, 'lchmod')
98 def _clean_up_path_for_archive(p):
99 # Not the most efficient approach.
102 # Take everything after any '/../'.
103 pos = result.rfind('/../')
105 result = result[result.rfind('/../') + 4:]
107 # Take everything after any remaining '../'.
108 if result.startswith("../"):
111 # Remove any '/./' sequences.
112 pos = result.find('/./')
114 result = result[0:pos] + '/' + result[pos + 3:]
115 pos = result.find('/./')
117 # Remove any leading '/'s.
118 result = result.lstrip('/')
120 # Replace '//' with '/' everywhere.
121 pos = result.find('//')
123 result = result[0:pos] + '/' + result[pos + 2:]
124 pos = result.find('//')
126 # Take everything after any remaining './'.
127 if result.startswith('./'):
130 # Take everything before any remaining '/.'.
131 if result.endswith('/.'):
134 if result == '' or result.endswith('/..'):
141 if p.startswith('/'):
143 if p.find('/../') != -1:
145 if p.startswith('../'):
147 if p.endswith('/..'):
152 def _clean_up_extract_path(p):
153 result = p.lstrip('/')
156 elif _risky_path(result):
162 # These tags are currently conceptually private to Metadata, and they
163 # must be unique, and must *never* be changed.
166 _rec_tag_common = 2 # times, user, group, type, perms, etc.
167 _rec_tag_symlink_target = 3
168 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
169 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e acls?
170 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
171 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
172 _rec_tag_hardlink_target = 8 # hard link target path
175 class ApplyError(Exception):
176 # Thrown when unable to apply any given bit of metadata to a path.
181 # Metadata is stored as a sequence of tagged binary records. Each
182 # record will have some subset of add, encode, load, create, and
183 # apply methods, i.e. _add_foo...
185 # We do allow an "empty" object as a special case, i.e. no
186 # records. One can be created by trying to write Metadata(), and
187 # for such an object, read() will return None. This is used by
188 # "bup save", for example, as a placeholder in cases where
191 # NOTE: if any relevant fields are added or removed, be sure to
192 # update same_file() below.
196 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
197 # must be non-negative and < 10**9.
199 def _add_common(self, path, st):
202 self.rdev = st.st_rdev
203 self.atime = st.st_atime
204 self.mtime = st.st_mtime
205 self.ctime = st.st_ctime
206 self.user = self.group = ''
207 entry = pwd_from_uid(st.st_uid)
209 self.user = entry.pw_name
210 entry = grp_from_gid(st.st_gid)
212 self.group = entry.gr_name
213 self.mode = st.st_mode
215 def _same_common(self, other):
216 """Return true or false to indicate similarity in the hardlink sense."""
217 return self.uid == other.uid \
218 and self.gid == other.gid \
219 and self.rdev == other.rdev \
220 and self.atime == other.atime \
221 and self.mtime == other.mtime \
222 and self.ctime == other.ctime \
223 and self.user == other.user \
224 and self.group == other.group
226 def _encode_common(self):
229 atime = xstat.nsecs_to_timespec(self.atime)
230 mtime = xstat.nsecs_to_timespec(self.mtime)
231 ctime = xstat.nsecs_to_timespec(self.ctime)
232 result = vint.pack('VVsVsVvVvVvV',
247 def _load_common_rec(self, port):
248 data = vint.read_bvec(port)
260 ctime_ns) = vint.unpack('VVsVsVvVvVvV', data)
261 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
262 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
263 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
265 def _recognized_file_type(self):
266 return stat.S_ISREG(self.mode) \
267 or stat.S_ISDIR(self.mode) \
268 or stat.S_ISCHR(self.mode) \
269 or stat.S_ISBLK(self.mode) \
270 or stat.S_ISFIFO(self.mode) \
271 or stat.S_ISSOCK(self.mode) \
272 or stat.S_ISLNK(self.mode)
274 def _create_via_common_rec(self, path, create_symlinks=True):
276 raise ApplyError('no metadata - cannot create path ' + path)
278 # If the path already exists and is a dir, try rmdir.
279 # If the path already exists and is anything else, try unlink.
282 st = xstat.lstat(path)
284 if e.errno != errno.ENOENT:
287 if stat.S_ISDIR(st.st_mode):
291 if e.errno == errno.ENOTEMPTY:
292 msg = 'refusing to overwrite non-empty dir ' + path
298 if stat.S_ISREG(self.mode):
299 assert(self._recognized_file_type())
300 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
302 elif stat.S_ISDIR(self.mode):
303 assert(self._recognized_file_type())
305 elif stat.S_ISCHR(self.mode):
306 assert(self._recognized_file_type())
307 os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
308 elif stat.S_ISBLK(self.mode):
309 assert(self._recognized_file_type())
310 os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
311 elif stat.S_ISFIFO(self.mode):
312 assert(self._recognized_file_type())
313 os.mknod(path, 0600 | stat.S_IFIFO)
314 elif stat.S_ISSOCK(self.mode):
315 os.mknod(path, 0600 | stat.S_IFSOCK)
316 elif stat.S_ISLNK(self.mode):
317 assert(self._recognized_file_type())
318 if self.symlink_target and create_symlinks:
319 # on MacOS, symlink() permissions depend on umask, and there's
320 # no way to chown a symlink after creating it, so we have to
322 oldumask = os.umask((self.mode & 0777) ^ 0777)
324 os.symlink(self.symlink_target, path)
327 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
329 assert(not self._recognized_file_type())
330 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
333 def _apply_common_rec(self, path, restore_numeric_ids=False):
335 raise ApplyError('no metadata - cannot apply to ' + path)
337 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
338 # EACCES errors at this stage are fatal for the current path.
339 if lutime and stat.S_ISLNK(self.mode):
341 lutime(path, (self.atime, self.mtime))
343 if e.errno == errno.EACCES:
344 raise ApplyError('lutime: %s' % e)
349 utime(path, (self.atime, self.mtime))
351 if e.errno == errno.EACCES:
352 raise ApplyError('utime: %s' % e)
356 # Implement tar/rsync-like semantics; see bup-restore(1).
357 # FIXME: should we consider caching user/group name <-> id
358 # mappings, getgroups(), etc.?
359 uid = gid = -1 # By default, do nothing.
363 if not restore_numeric_ids:
364 if self.uid != 0 and self.user:
365 entry = pwd_from_name(self.user)
368 if self.gid != 0 and self.group:
369 entry = grp_from_name(self.group)
372 else: # not superuser - only consider changing the group/gid
373 user_gids = os.getgroups()
374 if self.gid in user_gids:
376 if not restore_numeric_ids and self.gid != 0:
377 # The grp might not exist on the local system.
378 grps = filter(None, [grp_from_gid(x) for x in user_gids])
379 if self.group in [x.gr_name for x in grps]:
380 g = grp_from_name(self.group)
384 if uid != -1 or gid != -1:
386 os.lchown(path, uid, gid)
388 if e.errno == errno.EPERM:
389 add_error('lchown: %s' % e)
394 os.lchmod(path, stat.S_IMODE(self.mode))
395 elif not stat.S_ISLNK(self.mode):
396 os.chmod(path, stat.S_IMODE(self.mode))
401 def _encode_path(self):
403 return vint.pack('s', self.path)
407 def _load_path_rec(self, port):
408 self.path = vint.unpack('s', vint.read_bvec(port))[0]
413 def _add_symlink_target(self, path, st):
415 if stat.S_ISLNK(st.st_mode):
416 self.symlink_target = os.readlink(path)
418 add_error('readlink: %s', e)
420 def _encode_symlink_target(self):
421 return self.symlink_target
423 def _load_symlink_target_rec(self, port):
424 self.symlink_target = vint.read_bvec(port)
429 def _add_hardlink_target(self, target):
430 self.hardlink_target = target
432 def _same_hardlink_target(self, other):
433 """Return true or false to indicate similarity in the hardlink sense."""
434 return self.hardlink_target == other.hardlink_target
436 def _encode_hardlink_target(self):
437 return self.hardlink_target
439 def _load_hardlink_target_rec(self, port):
440 self.hardlink_target = vint.read_bvec(port)
443 ## POSIX1e ACL records
445 # Recorded as a list:
446 # [txt_id_acl, num_id_acl]
447 # or, if a directory:
448 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
449 # The numeric/text distinction only matters when reading/restoring
451 def _add_posix1e_acl(self, path, st):
452 if not posix1e: return
453 if not stat.S_ISLNK(st.st_mode):
455 if posix1e.has_extended(path):
456 acl = posix1e.ACL(file=path)
457 self.posix1e_acl = [acl, acl] # txt and num are the same
458 if stat.S_ISDIR(st.st_mode):
459 acl = posix1e.ACL(filedef=path)
460 self.posix1e_acl.extend([acl, acl])
461 except EnvironmentError, e:
462 if e.errno != errno.EOPNOTSUPP:
465 def _same_posix1e_acl(self, other):
466 """Return true or false to indicate similarity in the hardlink sense."""
467 return self.posix1e_acl == other.posix1e_acl
469 def _encode_posix1e_acl(self):
470 # Encode as two strings (w/default ACL string possibly empty).
472 acls = self.posix1e_acl
473 txt_flags = posix1e.TEXT_ABBREVIATE
474 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
475 acl_reps = [acls[0].to_any_text('', '\n', txt_flags),
476 acls[1].to_any_text('', '\n', num_flags)]
480 acl_reps.append(acls[2].to_any_text('', '\n', txt_flags))
481 acl_reps.append(acls[3].to_any_text('', '\n', num_flags))
482 return vint.pack('ssss',
483 acl_reps[0], acl_reps[1], acl_reps[2], acl_reps[3])
487 def _load_posix1e_acl_rec(self, port):
488 data = vint.read_bvec(port)
489 acl_reps = vint.unpack('ssss', data)
490 if acl_reps[2] == '':
491 acl_reps = acl_reps[:2]
492 self.posix1e_acl = [posix1e.ACL(text=x) for x in acl_reps]
494 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
495 def apply_acl(acl, kind):
497 acl.applyto(path, kind)
499 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
500 raise ApplyError('POSIX1e ACL applyto: %s' % e)
506 add_error("%s: can't restore ACLs; posix1e support missing.\n"
510 acls = self.posix1e_acl
512 if restore_numeric_ids:
513 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
515 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
516 if restore_numeric_ids:
517 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
519 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
522 ## Linux attributes (lsattr(1), chattr(1))
524 def _add_linux_attr(self, path, st):
525 if not get_linux_file_attr: return
526 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
528 attr = get_linux_file_attr(path)
530 self.linux_attr = attr
532 if e.errno == errno.EACCES:
533 add_error('read Linux attr: %s' % e)
534 elif e.errno == errno.ENOTTY or e.errno == errno.ENOSYS:
535 # ENOTTY: Function not implemented.
536 # ENOSYS: Inappropriate ioctl for device.
537 # Assume filesystem doesn't support attrs.
542 def _same_linux_attr(self, other):
543 """Return true or false to indicate similarity in the hardlink sense."""
544 return self.linux_attr == other.linux_attr
546 def _encode_linux_attr(self):
548 return vint.pack('V', self.linux_attr)
552 def _load_linux_attr_rec(self, port):
553 data = vint.read_bvec(port)
554 self.linux_attr = vint.unpack('V', data)[0]
556 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
558 if not set_linux_file_attr:
559 add_error("%s: can't restore linuxattrs: "
560 "linuxattr support missing.\n" % path)
563 set_linux_file_attr(path, self.linux_attr)
565 if e.errno == errno.ENOTTY or e.errno == errno.EOPNOTSUPP:
566 raise ApplyError('Linux chattr: %s' % e)
571 ## Linux extended attributes (getfattr(1), setfattr(1))
573 def _add_linux_xattr(self, path, st):
576 self.linux_xattr = xattr.get_all(path, nofollow=True)
577 except EnvironmentError, e:
578 if e.errno != errno.EOPNOTSUPP:
581 def _same_linux_xattr(self, other):
582 """Return true or false to indicate similarity in the hardlink sense."""
583 return self.linux_xattr == other.linux_xattr
585 def _encode_linux_xattr(self):
587 result = vint.pack('V', len(self.linux_xattr))
588 for name, value in self.linux_xattr:
589 result += vint.pack('ss', name, value)
594 def _load_linux_xattr_rec(self, file):
595 data = vint.read_bvec(file)
596 memfile = StringIO(data)
598 for i in range(vint.read_vuint(memfile)):
599 key = vint.read_bvec(memfile)
600 value = vint.read_bvec(memfile)
601 result.append((key, value))
602 self.linux_xattr = result
604 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
607 add_error("%s: can't restore xattr; xattr support missing.\n"
610 existing_xattrs = set(xattr.list(path, nofollow=True))
612 for k, v in self.linux_xattr:
613 if k not in existing_xattrs \
614 or v != xattr.get(path, k, nofollow=True):
616 xattr.set(path, k, v, nofollow=True)
618 if e.errno == errno.EPERM \
619 or e.errno == errno.EOPNOTSUPP:
620 raise ApplyError('xattr.set: %s' % e)
623 existing_xattrs -= frozenset([k])
624 for k in existing_xattrs:
626 xattr.remove(path, k, nofollow=True)
628 if e.errno == errno.EPERM:
629 raise ApplyError('xattr.remove: %s' % e)
638 self.symlink_target = None
639 self.hardlink_target = None
640 self.linux_attr = None
641 self.linux_xattr = None
642 self.posix1e_acl = None
643 self.posix1e_acl_default = None
645 def write(self, port, include_path=True):
646 records = include_path and [(_rec_tag_path, self._encode_path())] or []
647 records.extend([(_rec_tag_common, self._encode_common()),
648 (_rec_tag_symlink_target,
649 self._encode_symlink_target()),
650 (_rec_tag_hardlink_target,
651 self._encode_hardlink_target()),
652 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
653 (_rec_tag_linux_attr, self._encode_linux_attr()),
654 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
655 for tag, data in records:
657 vint.write_vuint(port, tag)
658 vint.write_bvec(port, data)
659 vint.write_vuint(port, _rec_tag_end)
661 def encode(self, include_path=True):
663 self.write(port, include_path)
664 return port.getvalue()
668 # This method should either return a valid Metadata object,
669 # return None if there was no information at all (just a
670 # _rec_tag_end), throw EOFError if there was nothing at all to
671 # read, or throw an Exception if a valid object could not be
673 tag = vint.read_vuint(port)
674 if tag == _rec_tag_end:
676 try: # From here on, EOF is an error.
678 while True: # only exit is error (exception) or _rec_tag_end
679 if tag == _rec_tag_path:
680 result._load_path_rec(port)
681 elif tag == _rec_tag_common:
682 result._load_common_rec(port)
683 elif tag == _rec_tag_symlink_target:
684 result._load_symlink_target_rec(port)
685 elif tag == _rec_tag_hardlink_target:
686 result._load_hardlink_target_rec(port)
687 elif tag == _rec_tag_posix1e_acl:
688 result._load_posix1e_acl_rec(port)
689 elif tag ==_rec_tag_nfsv4_acl:
690 result._load_nfsv4_acl_rec(port)
691 elif tag == _rec_tag_linux_attr:
692 result._load_linux_attr_rec(port)
693 elif tag == _rec_tag_linux_xattr:
694 result._load_linux_xattr_rec(port)
695 elif tag == _rec_tag_end:
697 else: # unknown record
699 tag = vint.read_vuint(port)
701 raise Exception("EOF while reading Metadata")
704 return stat.S_ISDIR(self.mode)
706 def create_path(self, path, create_symlinks=True):
707 self._create_via_common_rec(path, create_symlinks=create_symlinks)
709 def apply_to_path(self, path=None, restore_numeric_ids=False):
710 # apply metadata to path -- file must exist
714 raise Exception('Metadata.apply_to_path() called with no path')
715 if not self._recognized_file_type():
716 add_error('not applying metadata to "%s"' % path
717 + ' with unrecognized mode "0x%x"\n' % self.mode)
719 num_ids = restore_numeric_ids
721 self._apply_common_rec(path, restore_numeric_ids=num_ids)
722 self._apply_posix1e_acl_rec(path, restore_numeric_ids=num_ids)
723 self._apply_linux_attr_rec(path, restore_numeric_ids=num_ids)
724 self._apply_linux_xattr_rec(path, restore_numeric_ids=num_ids)
725 except ApplyError, e:
728 def same_file(self, other):
729 """Compare this to other for equivalency. Return true if
730 their information implies they could represent the same file
731 on disk, in the hardlink sense. Assume they're both regular
733 return self._same_common(other) \
734 and self._same_hardlink_target(other) \
735 and self._same_posix1e_acl(other) \
736 and self._same_linux_attr(other) \
737 and self._same_linux_xattr(other)
740 def from_path(path, statinfo=None, archive_path=None,
741 save_symlinks=True, hardlink_target=None):
743 result.path = archive_path
744 st = statinfo or xstat.lstat(path)
745 result.size = st.st_size
746 result._add_common(path, st)
748 result._add_symlink_target(path, st)
749 result._add_hardlink_target(hardlink_target)
750 result._add_posix1e_acl(path, st)
751 result._add_linux_attr(path, st)
752 result._add_linux_xattr(path, st)
756 def save_tree(output_file, paths,
762 # Issue top-level rewrite warnings.
764 safe_path = _clean_up_path_for_archive(path)
765 if safe_path != path:
766 log('archiving "%s" as "%s"\n' % (path, safe_path))
768 start_dir = os.getcwd()
770 for (p, st) in recursive_dirlist(paths, xdev=xdev):
771 dirlist_dir = os.getcwd()
773 safe_path = _clean_up_path_for_archive(p)
774 m = from_path(p, statinfo=st, archive_path=safe_path,
775 save_symlinks=save_symlinks)
777 print >> sys.stderr, m.path
778 m.write(output_file, include_path=write_paths)
779 os.chdir(dirlist_dir)
784 def _set_up_path(meta, create_symlinks=True):
785 # Allow directories to exist as a special case -- might have
786 # been created by an earlier longer path.
790 parent = os.path.dirname(meta.path)
793 meta.create_path(meta.path, create_symlinks=create_symlinks)
796 all_fields = frozenset(['path',
813 def summary_str(meta):
814 mode_val = xstat.mode_str(meta.mode)
817 user_val = str(meta.uid)
818 group_val = meta.group
820 group_val = str(meta.gid)
821 size_or_dev_val = '-'
822 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
823 size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
825 size_or_dev_val = meta.size
826 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
827 time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
828 path_val = meta.path or ''
829 if stat.S_ISLNK(meta.mode):
830 path_val += ' -> ' + meta.symlink_target
831 return '%-10s %-11s %11s %16s %s' % (mode_val,
832 user_val + "/" + group_val,
838 def detailed_str(meta, fields = None):
839 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
840 # 0", "link-target:", etc.
846 path = meta.path or ''
847 result.append('path: ' + path)
849 result.append('mode: %s (%s)' % (oct(meta.mode),
850 xstat.mode_str(meta.mode)))
851 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
852 result.append('link-target: ' + meta.symlink_target)
855 result.append('rdev: %d,%d' % (os.major(meta.rdev),
856 os.minor(meta.rdev)))
858 result.append('rdev: 0')
859 if 'size' in fields and meta.size:
860 result.append('size: ' + str(meta.size))
862 result.append('uid: ' + str(meta.uid))
864 result.append('gid: ' + str(meta.gid))
866 result.append('user: ' + meta.user)
867 if 'group' in fields:
868 result.append('group: ' + meta.group)
869 if 'atime' in fields:
870 # If we don't have xstat.lutime, that means we have to use
871 # utime(), and utime() has no way to set the mtime/atime of a
872 # symlink. Thus, the mtime/atime of a symlink is meaningless,
873 # so let's not report it. (That way scripts comparing
874 # before/after won't trigger.)
875 if xstat.lutime or not stat.S_ISLNK(meta.mode):
876 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
878 result.append('atime: 0')
879 if 'mtime' in fields:
880 if xstat.lutime or not stat.S_ISLNK(meta.mode):
881 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
883 result.append('mtime: 0')
884 if 'ctime' in fields:
885 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
886 if 'linux-attr' in fields and meta.linux_attr:
887 result.append('linux-attr: ' + hex(meta.linux_attr))
888 if 'linux-xattr' in fields and meta.linux_xattr:
889 for name, value in meta.linux_xattr:
890 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
891 if 'posix1e-acl' in fields and meta.posix1e_acl and posix1e:
892 flags = posix1e.TEXT_ABBREVIATE
893 if stat.S_ISDIR(meta.mode):
894 acl = meta.posix1e_acl[0]
895 default_acl = meta.posix1e_acl[2]
896 result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
897 result.append(acl.to_any_text('posix1e-acl-default: ', '\n', flags))
899 acl = meta.posix1e_acl[0]
900 result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
901 return '\n'.join(result)
904 class _ArchiveIterator:
907 return Metadata.read(self._file)
909 raise StopIteration()
914 def __init__(self, file):
918 def display_archive(file):
921 for meta in _ArchiveIterator(file):
924 print detailed_str(meta)
927 for meta in _ArchiveIterator(file):
928 print summary_str(meta)
930 for meta in _ArchiveIterator(file):
932 print >> sys.stderr, \
933 'bup: no metadata path, but asked to only display path', \
934 '(increase verbosity?)'
939 def start_extract(file, create_symlinks=True):
940 for meta in _ArchiveIterator(file):
941 if not meta: # Hit end record.
944 print >> sys.stderr, meta.path
945 xpath = _clean_up_extract_path(meta.path)
947 add_error(Exception('skipping risky path "%s"' % meta.path))
950 _set_up_path(meta, create_symlinks=create_symlinks)
953 def finish_extract(file, restore_numeric_ids=False):
955 for meta in _ArchiveIterator(file):
956 if not meta: # Hit end record.
958 xpath = _clean_up_extract_path(meta.path)
960 add_error(Exception('skipping risky path "%s"' % dir.path))
962 if os.path.isdir(meta.path):
963 all_dirs.append(meta)
966 print >> sys.stderr, meta.path
967 meta.apply_to_path(path=xpath,
968 restore_numeric_ids=restore_numeric_ids)
969 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
971 # Don't need to check xpath -- won't be in all_dirs if not OK.
972 xpath = _clean_up_extract_path(dir.path)
974 print >> sys.stderr, dir.path
975 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
978 def extract(file, restore_numeric_ids=False, create_symlinks=True):
979 # For now, just store all the directories and handle them last,
982 for meta in _ArchiveIterator(file):
983 if not meta: # Hit end record.
985 xpath = _clean_up_extract_path(meta.path)
987 add_error(Exception('skipping risky path "%s"' % meta.path))
991 print >> sys.stderr, '+', meta.path
992 _set_up_path(meta, create_symlinks=create_symlinks)
993 if os.path.isdir(meta.path):
994 all_dirs.append(meta)
997 print >> sys.stderr, '=', meta.path
998 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
999 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1000 for dir in all_dirs:
1001 # Don't need to check xpath -- won't be in all_dirs if not OK.
1002 xpath = _clean_up_extract_path(dir.path)
1004 print >> sys.stderr, '=', xpath
1005 # Shouldn't have to check for risky paths here (omitted above).
1006 dir.apply_to_path(path=dir.path,
1007 restore_numeric_ids=restore_numeric_ids)