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
18 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
23 except AttributeError:
24 log('Warning: python-xattr module is too old; '
25 'install python-pyxattr instead.\n')
30 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
33 from bup._helpers import get_linux_file_attr, set_linux_file_attr
35 # No need for a warning here; the only reason they won't exist is that we're
36 # not on Linux, in which case files don't have any linux attrs anyway, so
37 # lacking the functions isn't a problem.
38 get_linux_file_attr = set_linux_file_attr = None
41 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
43 # Q: Consider hardlink support?
44 # Q: Is it OK to store raw linux attr (chattr) flags?
45 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
46 # Q: Is the application of posix1e has_extended() correct?
47 # Q: Is one global --numeric-ids argument sufficient?
48 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
49 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
51 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
52 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
53 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
54 # FIXME: Consider pack('vvvvsss', ...) optimization.
55 # FIXME: Consider caching users/groups.
59 # osx (varies between hfs and hfs+):
60 # type - regular dir char block fifo socket ...
61 # perms - rwxrwxrwxsgt
62 # times - ctime atime mtime
65 # hard-link-info (hfs+ only)
68 # attributes-osx see chflags
74 # type - regular dir ...
75 # times - creation, modification, posix change, access
78 # attributes - see attrib
80 # forks (alternate data streams)
84 # type - regular dir ...
85 # perms - rwxrwxrwx (maybe - see wikipedia)
86 # times - creation, modification, access
87 # attributes - see attrib
91 _have_lchmod = hasattr(os, 'lchmod')
94 def _clean_up_path_for_archive(p):
95 # Not the most efficient approach.
98 # Take everything after any '/../'.
99 pos = result.rfind('/../')
101 result = result[result.rfind('/../') + 4:]
103 # Take everything after any remaining '../'.
104 if result.startswith("../"):
107 # Remove any '/./' sequences.
108 pos = result.find('/./')
110 result = result[0:pos] + '/' + result[pos + 3:]
111 pos = result.find('/./')
113 # Remove any leading '/'s.
114 result = result.lstrip('/')
116 # Replace '//' with '/' everywhere.
117 pos = result.find('//')
119 result = result[0:pos] + '/' + result[pos + 2:]
120 pos = result.find('//')
122 # Take everything after any remaining './'.
123 if result.startswith('./'):
126 # Take everything before any remaining '/.'.
127 if result.endswith('/.'):
130 if result == '' or result.endswith('/..'):
137 if p.startswith('/'):
139 if p.find('/../') != -1:
141 if p.startswith('../'):
143 if p.endswith('/..'):
148 def _clean_up_extract_path(p):
149 result = p.lstrip('/')
152 elif _risky_path(result):
158 # These tags are currently conceptually private to Metadata, and they
159 # must be unique, and must *never* be changed.
162 _rec_tag_common = 2 # times, user, group, type, perms, etc.
163 _rec_tag_symlink_target = 3
164 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
165 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e acls?
166 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
167 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
168 _rec_tag_hardlink_target = 8 # hard link target path
171 class ApplyError(Exception):
172 # Thrown when unable to apply any given bit of metadata to a path.
177 # Metadata is stored as a sequence of tagged binary records. Each
178 # record will have some subset of add, encode, load, create, and
179 # apply methods, i.e. _add_foo...
181 # We do allow an "empty" object as a special case, i.e. no
182 # records. One can be created by trying to write Metadata(), and
183 # for such an object, read() will return None. This is used by
184 # "bup save", for example, as a placeholder in cases where
187 # NOTE: if any relevant fields are added or removed, be sure to
188 # update same_file() below.
192 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
193 # must be non-negative and < 10**9.
195 def _add_common(self, path, st):
198 self.rdev = st.st_rdev
199 self.atime = st.st_atime
200 self.mtime = st.st_mtime
201 self.ctime = st.st_ctime
202 self.user = self.group = ''
203 entry = pwd_from_uid(st.st_uid)
205 self.user = entry.pw_name
206 entry = grp_from_gid(st.st_gid)
208 self.group = entry.gr_name
209 self.mode = st.st_mode
211 def _same_common(self, other):
212 """Return true or false to indicate similarity in the hardlink sense."""
213 return self.uid == other.uid \
214 and self.gid == other.gid \
215 and self.rdev == other.rdev \
216 and self.atime == other.atime \
217 and self.mtime == other.mtime \
218 and self.ctime == other.ctime \
219 and self.user == other.user \
220 and self.group == other.group
222 def _encode_common(self):
225 atime = xstat.nsecs_to_timespec(self.atime)
226 mtime = xstat.nsecs_to_timespec(self.mtime)
227 ctime = xstat.nsecs_to_timespec(self.ctime)
228 result = vint.pack('VVsVsVvVvVvV',
243 def _load_common_rec(self, port):
244 data = vint.read_bvec(port)
256 ctime_ns) = vint.unpack('VVsVsVvVvVvV', data)
257 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
258 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
259 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
261 def _recognized_file_type(self):
262 return stat.S_ISREG(self.mode) \
263 or stat.S_ISDIR(self.mode) \
264 or stat.S_ISCHR(self.mode) \
265 or stat.S_ISBLK(self.mode) \
266 or stat.S_ISFIFO(self.mode) \
267 or stat.S_ISSOCK(self.mode) \
268 or stat.S_ISLNK(self.mode)
270 def _create_via_common_rec(self, path, create_symlinks=True):
272 raise ApplyError('no metadata - cannot create path ' + path)
274 # If the path already exists and is a dir, try rmdir.
275 # If the path already exists and is anything else, try unlink.
278 st = xstat.lstat(path)
280 if e.errno != errno.ENOENT:
283 if stat.S_ISDIR(st.st_mode):
287 if e.errno == errno.ENOTEMPTY:
288 msg = 'refusing to overwrite non-empty dir ' + path
294 if stat.S_ISREG(self.mode):
295 assert(self._recognized_file_type())
296 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
298 elif stat.S_ISDIR(self.mode):
299 assert(self._recognized_file_type())
301 elif stat.S_ISCHR(self.mode):
302 assert(self._recognized_file_type())
303 os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
304 elif stat.S_ISBLK(self.mode):
305 assert(self._recognized_file_type())
306 os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
307 elif stat.S_ISFIFO(self.mode):
308 assert(self._recognized_file_type())
309 os.mknod(path, 0600 | stat.S_IFIFO)
310 elif stat.S_ISSOCK(self.mode):
311 os.mknod(path, 0600 | stat.S_IFSOCK)
312 elif stat.S_ISLNK(self.mode):
313 assert(self._recognized_file_type())
314 if self.symlink_target and create_symlinks:
315 # on MacOS, symlink() permissions depend on umask, and there's
316 # no way to chown a symlink after creating it, so we have to
318 oldumask = os.umask((self.mode & 0777) ^ 0777)
320 os.symlink(self.symlink_target, path)
323 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
325 assert(not self._recognized_file_type())
326 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
329 def _apply_common_rec(self, path, restore_numeric_ids=False):
331 raise ApplyError('no metadata - cannot apply to ' + path)
333 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
334 # EACCES errors at this stage are fatal for the current path.
335 if lutime and stat.S_ISLNK(self.mode):
337 lutime(path, (self.atime, self.mtime))
339 if e.errno == errno.EACCES:
340 raise ApplyError('lutime: %s' % e)
345 utime(path, (self.atime, self.mtime))
347 if e.errno == errno.EACCES:
348 raise ApplyError('utime: %s' % e)
352 # Implement tar/rsync-like semantics; see bup-restore(1).
353 # FIXME: should we consider caching user/group name <-> id
354 # mappings, getgroups(), etc.?
355 uid = gid = -1 # By default, do nothing.
359 if not restore_numeric_ids:
360 if self.uid != 0 and self.user:
361 entry = pwd_from_name(self.user)
364 if self.gid != 0 and self.group:
365 entry = grp_from_name(self.group)
368 else: # not superuser - only consider changing the group/gid
369 user_gids = os.getgroups()
370 if self.gid in user_gids:
372 if not restore_numeric_ids and self.gid != 0:
373 # The grp might not exist on the local system.
374 grps = filter(None, [grp_from_gid(x) for x in user_gids])
375 if self.group in [x.gr_name for x in grps]:
376 g = grp_from_name(self.group)
380 if uid != -1 or gid != -1:
382 os.lchown(path, uid, gid)
384 if e.errno == errno.EPERM:
385 add_error('lchown: %s' % e)
390 os.lchmod(path, stat.S_IMODE(self.mode))
391 elif not stat.S_ISLNK(self.mode):
392 os.chmod(path, stat.S_IMODE(self.mode))
397 def _encode_path(self):
399 return vint.pack('s', self.path)
403 def _load_path_rec(self, port):
404 self.path = vint.unpack('s', vint.read_bvec(port))[0]
409 def _add_symlink_target(self, path, st):
411 if stat.S_ISLNK(st.st_mode):
412 self.symlink_target = os.readlink(path)
414 add_error('readlink: %s', e)
416 def _encode_symlink_target(self):
417 return self.symlink_target
419 def _load_symlink_target_rec(self, port):
420 self.symlink_target = vint.read_bvec(port)
425 def _add_hardlink_target(self, target):
426 self.hardlink_target = target
428 def _same_hardlink_target(self, other):
429 """Return true or false to indicate similarity in the hardlink sense."""
430 return self.hardlink_target == other.hardlink_target
432 def _encode_hardlink_target(self):
433 return self.hardlink_target
435 def _load_hardlink_target_rec(self, port):
436 self.hardlink_target = vint.read_bvec(port)
439 ## POSIX1e ACL records
441 # Recorded as a list:
442 # [txt_id_acl, num_id_acl]
443 # or, if a directory:
444 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
445 # The numeric/text distinction only matters when reading/restoring
447 def _add_posix1e_acl(self, path, st):
448 if not posix1e: return
449 if not stat.S_ISLNK(st.st_mode):
451 if posix1e.has_extended(path):
452 acl = posix1e.ACL(file=path)
453 self.posix1e_acl = [acl, acl] # txt and num are the same
454 if stat.S_ISDIR(st.st_mode):
455 acl = posix1e.ACL(filedef=path)
456 self.posix1e_acl.extend([acl, acl])
457 except EnvironmentError, e:
458 if e.errno != errno.EOPNOTSUPP:
461 def _same_posix1e_acl(self, other):
462 """Return true or false to indicate similarity in the hardlink sense."""
463 return self.posix1e_acl == other.posix1e_acl
465 def _encode_posix1e_acl(self):
466 # Encode as two strings (w/default ACL string possibly empty).
468 acls = self.posix1e_acl
469 txt_flags = posix1e.TEXT_ABBREVIATE
470 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
471 acl_reps = [acls[0].to_any_text('', '\n', txt_flags),
472 acls[1].to_any_text('', '\n', num_flags)]
476 acl_reps.append(acls[2].to_any_text('', '\n', txt_flags))
477 acl_reps.append(acls[3].to_any_text('', '\n', num_flags))
478 return vint.pack('ssss',
479 acl_reps[0], acl_reps[1], acl_reps[2], acl_reps[3])
483 def _load_posix1e_acl_rec(self, port):
484 data = vint.read_bvec(port)
485 acl_reps = vint.unpack('ssss', data)
486 if acl_reps[2] == '':
487 acl_reps = acl_reps[:2]
488 self.posix1e_acl = [posix1e.ACL(text=x) for x in acl_reps]
490 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
491 def apply_acl(acl, kind):
493 acl.applyto(path, kind)
495 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
496 raise ApplyError('POSIX1e ACL applyto: %s' % e)
502 add_error("%s: can't restore ACLs; posix1e support missing.\n"
506 acls = self.posix1e_acl
508 if restore_numeric_ids:
509 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
511 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
512 if restore_numeric_ids:
513 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
515 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
518 ## Linux attributes (lsattr(1), chattr(1))
520 def _add_linux_attr(self, path, st):
521 if not get_linux_file_attr: return
522 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
524 attr = get_linux_file_attr(path)
526 self.linux_attr = attr
528 if e.errno == errno.EACCES:
529 add_error('read Linux attr: %s' % e)
530 elif e.errno == errno.ENOTTY or e.errno == errno.ENOSYS:
531 # ENOTTY: Function not implemented.
532 # ENOSYS: Inappropriate ioctl for device.
533 # Assume filesystem doesn't support attrs.
538 def _same_linux_attr(self, other):
539 """Return true or false to indicate similarity in the hardlink sense."""
540 return self.linux_attr == other.linux_attr
542 def _encode_linux_attr(self):
544 return vint.pack('V', self.linux_attr)
548 def _load_linux_attr_rec(self, port):
549 data = vint.read_bvec(port)
550 self.linux_attr = vint.unpack('V', data)[0]
552 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
554 if not set_linux_file_attr:
555 add_error("%s: can't restore linuxattrs: "
556 "linuxattr support missing.\n" % path)
559 set_linux_file_attr(path, self.linux_attr)
561 if e.errno == errno.ENOTTY:
562 raise ApplyError('Linux chattr: %s' % e)
567 ## Linux extended attributes (getfattr(1), setfattr(1))
569 def _add_linux_xattr(self, path, st):
572 self.linux_xattr = xattr.get_all(path, nofollow=True)
573 except EnvironmentError, e:
574 if e.errno != errno.EOPNOTSUPP:
577 def _same_linux_xattr(self, other):
578 """Return true or false to indicate similarity in the hardlink sense."""
579 return self.linux_xattr == other.linux_xattr
581 def _encode_linux_xattr(self):
583 result = vint.pack('V', len(self.linux_xattr))
584 for name, value in self.linux_xattr:
585 result += vint.pack('ss', name, value)
590 def _load_linux_xattr_rec(self, file):
591 data = vint.read_bvec(file)
592 memfile = StringIO(data)
594 for i in range(vint.read_vuint(memfile)):
595 key = vint.read_bvec(memfile)
596 value = vint.read_bvec(memfile)
597 result.append((key, value))
598 self.linux_xattr = result
600 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
603 add_error("%s: can't restore xattr; xattr support missing.\n"
606 existing_xattrs = set(xattr.list(path, nofollow=True))
608 for k, v in self.linux_xattr:
609 if k not in existing_xattrs \
610 or v != xattr.get(path, k, nofollow=True):
612 xattr.set(path, k, v, nofollow=True)
614 if e.errno == errno.EPERM \
615 or e.errno == errno.EOPNOTSUPP:
616 raise ApplyError('xattr.set: %s' % e)
619 existing_xattrs -= frozenset([k])
620 for k in existing_xattrs:
622 xattr.remove(path, k, nofollow=True)
624 if e.errno == errno.EPERM:
625 raise ApplyError('xattr.remove: %s' % e)
634 self.symlink_target = None
635 self.hardlink_target = None
636 self.linux_attr = None
637 self.linux_xattr = None
638 self.posix1e_acl = None
639 self.posix1e_acl_default = None
641 def write(self, port, include_path=True):
642 records = include_path and [(_rec_tag_path, self._encode_path())] or []
643 records.extend([(_rec_tag_common, self._encode_common()),
644 (_rec_tag_symlink_target,
645 self._encode_symlink_target()),
646 (_rec_tag_hardlink_target,
647 self._encode_hardlink_target()),
648 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
649 (_rec_tag_linux_attr, self._encode_linux_attr()),
650 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
651 for tag, data in records:
653 vint.write_vuint(port, tag)
654 vint.write_bvec(port, data)
655 vint.write_vuint(port, _rec_tag_end)
657 def encode(self, include_path=True):
659 self.write(port, include_path)
660 return port.getvalue()
664 # This method should either return a valid Metadata object,
665 # return None if there was no information at all (just a
666 # _rec_tag_end), throw EOFError if there was nothing at all to
667 # read, or throw an Exception if a valid object could not be
669 tag = vint.read_vuint(port)
670 if tag == _rec_tag_end:
672 try: # From here on, EOF is an error.
674 while True: # only exit is error (exception) or _rec_tag_end
675 if tag == _rec_tag_path:
676 result._load_path_rec(port)
677 elif tag == _rec_tag_common:
678 result._load_common_rec(port)
679 elif tag == _rec_tag_symlink_target:
680 result._load_symlink_target_rec(port)
681 elif tag == _rec_tag_hardlink_target:
682 result._load_hardlink_target_rec(port)
683 elif tag == _rec_tag_posix1e_acl:
684 result._load_posix1e_acl_rec(port)
685 elif tag ==_rec_tag_nfsv4_acl:
686 result._load_nfsv4_acl_rec(port)
687 elif tag == _rec_tag_linux_attr:
688 result._load_linux_attr_rec(port)
689 elif tag == _rec_tag_linux_xattr:
690 result._load_linux_xattr_rec(port)
691 elif tag == _rec_tag_end:
693 else: # unknown record
695 tag = vint.read_vuint(port)
697 raise Exception("EOF while reading Metadata")
700 return stat.S_ISDIR(self.mode)
702 def create_path(self, path, create_symlinks=True):
703 self._create_via_common_rec(path, create_symlinks=create_symlinks)
705 def apply_to_path(self, path=None, restore_numeric_ids=False):
706 # apply metadata to path -- file must exist
710 raise Exception('Metadata.apply_to_path() called with no path')
711 if not self._recognized_file_type():
712 add_error('not applying metadata to "%s"' % path
713 + ' with unrecognized mode "0x%x"\n' % self.mode)
715 num_ids = restore_numeric_ids
717 self._apply_common_rec(path, restore_numeric_ids=num_ids)
718 self._apply_posix1e_acl_rec(path, restore_numeric_ids=num_ids)
719 self._apply_linux_attr_rec(path, restore_numeric_ids=num_ids)
720 self._apply_linux_xattr_rec(path, restore_numeric_ids=num_ids)
721 except ApplyError, e:
724 def same_file(self, other):
725 """Compare this to other for equivalency. Return true if
726 their information implies they could represent the same file
727 on disk, in the hardlink sense. Assume they're both regular
729 return self._same_common(other) \
730 and self._same_hardlink_target(other) \
731 and self._same_posix1e_acl(other) \
732 and self._same_linux_attr(other) \
733 and self._same_linux_xattr(other)
736 def from_path(path, statinfo=None, archive_path=None,
737 save_symlinks=True, hardlink_target=None):
739 result.path = archive_path
740 st = statinfo or xstat.lstat(path)
741 result.size = st.st_size
742 result._add_common(path, st)
744 result._add_symlink_target(path, st)
745 result._add_hardlink_target(hardlink_target)
746 result._add_posix1e_acl(path, st)
747 result._add_linux_attr(path, st)
748 result._add_linux_xattr(path, st)
752 def save_tree(output_file, paths,
758 # Issue top-level rewrite warnings.
760 safe_path = _clean_up_path_for_archive(path)
761 if safe_path != path:
762 log('archiving "%s" as "%s"\n' % (path, safe_path))
764 start_dir = os.getcwd()
766 for (p, st) in recursive_dirlist(paths, xdev=xdev):
767 dirlist_dir = os.getcwd()
769 safe_path = _clean_up_path_for_archive(p)
770 m = from_path(p, statinfo=st, archive_path=safe_path,
771 save_symlinks=save_symlinks)
773 print >> sys.stderr, m.path
774 m.write(output_file, include_path=write_paths)
775 os.chdir(dirlist_dir)
780 def _set_up_path(meta, create_symlinks=True):
781 # Allow directories to exist as a special case -- might have
782 # been created by an earlier longer path.
786 parent = os.path.dirname(meta.path)
789 meta.create_path(meta.path, create_symlinks=create_symlinks)
792 all_fields = frozenset(['path',
809 def summary_str(meta):
810 mode_val = xstat.mode_str(meta.mode)
813 user_val = str(meta.uid)
814 group_val = meta.group
816 group_val = str(meta.gid)
817 size_or_dev_val = '-'
818 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
819 size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
821 size_or_dev_val = meta.size
822 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
823 time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
824 path_val = meta.path or ''
825 if stat.S_ISLNK(meta.mode):
826 path_val += ' -> ' + meta.symlink_target
827 return '%-10s %-11s %11s %16s %s' % (mode_val,
828 user_val + "/" + group_val,
834 def detailed_str(meta, fields = None):
835 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
836 # 0", "link-target:", etc.
842 path = meta.path or ''
843 result.append('path: ' + path)
845 result.append('mode: %s (%s)' % (oct(meta.mode),
846 xstat.mode_str(meta.mode)))
847 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
848 result.append('link-target: ' + meta.symlink_target)
851 result.append('rdev: %d,%d' % (os.major(meta.rdev),
852 os.minor(meta.rdev)))
854 result.append('rdev: 0')
855 if 'size' in fields and meta.size:
856 result.append('size: ' + str(meta.size))
858 result.append('uid: ' + str(meta.uid))
860 result.append('gid: ' + str(meta.gid))
862 result.append('user: ' + meta.user)
863 if 'group' in fields:
864 result.append('group: ' + meta.group)
865 if 'atime' in fields:
866 # If we don't have xstat.lutime, that means we have to use
867 # utime(), and utime() has no way to set the mtime/atime of a
868 # symlink. Thus, the mtime/atime of a symlink is meaningless,
869 # so let's not report it. (That way scripts comparing
870 # before/after won't trigger.)
871 if xstat.lutime or not stat.S_ISLNK(meta.mode):
872 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
874 result.append('atime: 0')
875 if 'mtime' in fields:
876 if xstat.lutime or not stat.S_ISLNK(meta.mode):
877 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
879 result.append('mtime: 0')
880 if 'ctime' in fields:
881 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
882 if 'linux-attr' in fields and meta.linux_attr:
883 result.append('linux-attr: ' + hex(meta.linux_attr))
884 if 'linux-xattr' in fields and meta.linux_xattr:
885 for name, value in meta.linux_xattr:
886 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
887 if 'posix1e-acl' in fields and meta.posix1e_acl and posix1e:
888 flags = posix1e.TEXT_ABBREVIATE
889 if stat.S_ISDIR(meta.mode):
890 acl = meta.posix1e_acl[0]
891 default_acl = meta.posix1e_acl[2]
892 result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
893 result.append(acl.to_any_text('posix1e-acl-default: ', '\n', flags))
895 acl = meta.posix1e_acl[0]
896 result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
897 return '\n'.join(result)
900 class _ArchiveIterator:
903 return Metadata.read(self._file)
905 raise StopIteration()
910 def __init__(self, file):
914 def display_archive(file):
917 for meta in _ArchiveIterator(file):
920 print detailed_str(meta)
923 for meta in _ArchiveIterator(file):
924 print summary_str(meta)
926 for meta in _ArchiveIterator(file):
928 print >> sys.stderr, \
929 'bup: no metadata path, but asked to only display path', \
930 '(increase verbosity?)'
935 def start_extract(file, create_symlinks=True):
936 for meta in _ArchiveIterator(file):
937 if not meta: # Hit end record.
940 print >> sys.stderr, meta.path
941 xpath = _clean_up_extract_path(meta.path)
943 add_error(Exception('skipping risky path "%s"' % meta.path))
946 _set_up_path(meta, create_symlinks=create_symlinks)
949 def finish_extract(file, restore_numeric_ids=False):
951 for meta in _ArchiveIterator(file):
952 if not meta: # Hit end record.
954 xpath = _clean_up_extract_path(meta.path)
956 add_error(Exception('skipping risky path "%s"' % dir.path))
958 if os.path.isdir(meta.path):
959 all_dirs.append(meta)
962 print >> sys.stderr, meta.path
963 meta.apply_to_path(path=xpath,
964 restore_numeric_ids=restore_numeric_ids)
965 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
967 # Don't need to check xpath -- won't be in all_dirs if not OK.
968 xpath = _clean_up_extract_path(dir.path)
970 print >> sys.stderr, dir.path
971 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
974 def extract(file, restore_numeric_ids=False, create_symlinks=True):
975 # For now, just store all the directories and handle them last,
978 for meta in _ArchiveIterator(file):
979 if not meta: # Hit end record.
981 xpath = _clean_up_extract_path(meta.path)
983 add_error(Exception('skipping risky path "%s"' % meta.path))
987 print >> sys.stderr, '+', meta.path
988 _set_up_path(meta, create_symlinks=create_symlinks)
989 if os.path.isdir(meta.path):
990 all_dirs.append(meta)
993 print >> sys.stderr, '=', meta.path
994 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
995 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
997 # Don't need to check xpath -- won't be in all_dirs if not OK.
998 xpath = _clean_up_extract_path(dir.path)
1000 print >> sys.stderr, '=', xpath
1001 # Shouldn't have to check for risky paths here (omitted above).
1002 dir.apply_to_path(path=dir.path,
1003 restore_numeric_ids=restore_numeric_ids)