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.xstat import utime, lutime
17 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
22 except AttributeError:
23 log('Warning: python-xattr module is too old; '
24 'install python-pyxattr instead.\n')
29 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
32 from bup._helpers import get_linux_file_attr, set_linux_file_attr
34 # No need for a warning here; the only reason they won't exist is that we're
35 # not on Linux, in which case files don't have any linux attrs anyway, so
36 # lacking the functions isn't a problem.
37 get_linux_file_attr = set_linux_file_attr = None
40 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
42 # Q: Consider hardlink support?
43 # Q: Is it OK to store raw linux attr (chattr) flags?
44 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
45 # Q: Is the application of posix1e has_extended() correct?
46 # Q: Is one global --numeric-ids argument sufficient?
47 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
48 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
50 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
51 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
52 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
53 # FIXME: Consider pack('vvvvsss', ...) optimization.
54 # FIXME: Consider caching users/groups.
58 # osx (varies between hfs and hfs+):
59 # type - regular dir char block fifo socket ...
60 # perms - rwxrwxrwxsgt
61 # times - ctime atime mtime
64 # hard-link-info (hfs+ only)
67 # attributes-osx see chflags
73 # type - regular dir ...
74 # times - creation, modification, posix change, access
77 # attributes - see attrib
79 # forks (alternate data streams)
83 # type - regular dir ...
84 # perms - rwxrwxrwx (maybe - see wikipedia)
85 # times - creation, modification, access
86 # attributes - see attrib
90 _have_lchmod = hasattr(os, 'lchmod')
93 def _clean_up_path_for_archive(p):
94 # Not the most efficient approach.
97 # Take everything after any '/../'.
98 pos = result.rfind('/../')
100 result = result[result.rfind('/../') + 4:]
102 # Take everything after any remaining '../'.
103 if result.startswith("../"):
106 # Remove any '/./' sequences.
107 pos = result.find('/./')
109 result = result[0:pos] + '/' + result[pos + 3:]
110 pos = result.find('/./')
112 # Remove any leading '/'s.
113 result = result.lstrip('/')
115 # Replace '//' with '/' everywhere.
116 pos = result.find('//')
118 result = result[0:pos] + '/' + result[pos + 2:]
119 pos = result.find('//')
121 # Take everything after any remaining './'.
122 if result.startswith('./'):
125 # Take everything before any remaining '/.'.
126 if result.endswith('/.'):
129 if result == '' or result.endswith('/..'):
136 if p.startswith('/'):
138 if p.find('/../') != -1:
140 if p.startswith('../'):
142 if p.endswith('/..'):
147 def _clean_up_extract_path(p):
148 result = p.lstrip('/')
151 elif _risky_path(result):
157 # These tags are currently conceptually private to Metadata, and they
158 # must be unique, and must *never* be changed.
161 _rec_tag_common = 2 # times, user, group, type, perms, etc.
162 _rec_tag_symlink_target = 3
163 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
164 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e acls?
165 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
166 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
167 _rec_tag_hardlink_target = 8 # hard link target path
170 class ApplyError(Exception):
171 # Thrown when unable to apply any given bit of metadata to a path.
176 # Metadata is stored as a sequence of tagged binary records. Each
177 # record will have some subset of add, encode, load, create, and
178 # apply methods, i.e. _add_foo...
180 # We do allow an "empty" object as a special case, i.e. no
181 # records. One can be created by trying to write Metadata(), and
182 # for such an object, read() will return None. This is used by
183 # "bup save", for example, as a placeholder in cases where
186 # NOTE: if any relevant fields are added or removed, be sure to
187 # update same_file() below.
191 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
192 # must be non-negative and < 10**9.
194 def _add_common(self, path, st):
197 self.rdev = st.st_rdev
198 self.atime = st.st_atime
199 self.mtime = st.st_mtime
200 self.ctime = st.st_ctime
201 self.user = self.group = ''
202 # FIXME: should we be caching id -> user/group name mappings?
203 # IIRC, tar uses some trick -- possibly caching the last pair.
205 self.user = pwd.getpwuid(st.st_uid)[0]
209 self.group = grp.getgrgid(st.st_gid)[0]
212 self.mode = st.st_mode
214 def _same_common(self, other):
215 """Return true or false to indicate similarity in the hardlink sense."""
216 return self.uid == other.uid \
217 and self.gid == other.gid \
218 and self.rdev == other.rdev \
219 and self.atime == other.atime \
220 and self.mtime == other.mtime \
221 and self.ctime == other.ctime \
222 and self.user == other.user \
223 and self.group == other.group
225 def _encode_common(self):
228 atime = xstat.nsecs_to_timespec(self.atime)
229 mtime = xstat.nsecs_to_timespec(self.mtime)
230 ctime = xstat.nsecs_to_timespec(self.ctime)
231 result = vint.pack('VVsVsVvVvVvV',
246 def _load_common_rec(self, port):
247 data = vint.read_bvec(port)
259 ctime_ns) = vint.unpack('VVsVsVvVvVvV', data)
260 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
261 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
262 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
264 def _recognized_file_type(self):
265 return stat.S_ISREG(self.mode) \
266 or stat.S_ISDIR(self.mode) \
267 or stat.S_ISCHR(self.mode) \
268 or stat.S_ISBLK(self.mode) \
269 or stat.S_ISFIFO(self.mode) \
270 or stat.S_ISSOCK(self.mode) \
271 or stat.S_ISLNK(self.mode)
273 def _create_via_common_rec(self, path, create_symlinks=True):
275 raise ApplyError('no metadata - cannot create path ' + path)
277 # If the path already exists and is a dir, try rmdir.
278 # If the path already exists and is anything else, try unlink.
281 st = xstat.lstat(path)
283 if e.errno != errno.ENOENT:
286 if stat.S_ISDIR(st.st_mode):
290 if e.errno == errno.ENOTEMPTY:
291 msg = 'refusing to overwrite non-empty dir ' + path
297 if stat.S_ISREG(self.mode):
298 assert(self._recognized_file_type())
299 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
301 elif stat.S_ISDIR(self.mode):
302 assert(self._recognized_file_type())
304 elif stat.S_ISCHR(self.mode):
305 assert(self._recognized_file_type())
306 os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
307 elif stat.S_ISBLK(self.mode):
308 assert(self._recognized_file_type())
309 os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
310 elif stat.S_ISFIFO(self.mode):
311 assert(self._recognized_file_type())
312 os.mknod(path, 0600 | stat.S_IFIFO)
313 elif stat.S_ISSOCK(self.mode):
314 os.mknod(path, 0600 | stat.S_IFSOCK)
315 elif stat.S_ISLNK(self.mode):
316 assert(self._recognized_file_type())
317 if self.symlink_target and create_symlinks:
318 # on MacOS, symlink() permissions depend on umask, and there's
319 # no way to chown a symlink after creating it, so we have to
321 oldumask = os.umask((self.mode & 0777) ^ 0777)
323 os.symlink(self.symlink_target, path)
326 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
328 assert(not self._recognized_file_type())
329 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
332 def _apply_common_rec(self, path, restore_numeric_ids=False):
334 raise ApplyError('no metadata - cannot apply to ' + path)
336 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
337 # EACCES errors at this stage are fatal for the current path.
338 if lutime and stat.S_ISLNK(self.mode):
340 lutime(path, (self.atime, self.mtime))
342 if e.errno == errno.EACCES:
343 raise ApplyError('lutime: %s' % e)
348 utime(path, (self.atime, self.mtime))
350 if e.errno == errno.EACCES:
351 raise ApplyError('utime: %s' % e)
355 # Implement tar/rsync-like semantics; see bup-restore(1).
356 # FIXME: should we consider caching user/group name <-> id
357 # mappings, getgroups(), etc.?
358 uid = gid = -1 # By default, do nothing.
362 if not restore_numeric_ids:
363 if self.uid != 0 and self.user:
365 uid = pwd.getpwnam(self.user)[2]
367 pass # Fall back to self.uid.
368 if self.gid != 0 and self.group:
370 gid = grp.getgrnam(self.group)[2]
372 pass # Fall back to self.gid.
373 else: # not superuser - only consider changing the group/gid
374 user_gids = os.getgroups()
375 if self.gid in user_gids:
377 if not restore_numeric_ids and \
379 self.group in [grp.getgrgid(x)[0] for x in user_gids]:
381 gid = grp.getgrnam(self.group)[2]
383 pass # Fall back to gid.
385 if uid != -1 or gid != -1:
387 os.lchown(path, uid, gid)
389 if e.errno == errno.EPERM:
390 add_error('lchown: %s' % e)
395 os.lchmod(path, stat.S_IMODE(self.mode))
396 elif not stat.S_ISLNK(self.mode):
397 os.chmod(path, stat.S_IMODE(self.mode))
402 def _encode_path(self):
404 return vint.pack('s', self.path)
408 def _load_path_rec(self, port):
409 self.path = vint.unpack('s', vint.read_bvec(port))[0]
414 def _add_symlink_target(self, path, st):
416 if stat.S_ISLNK(st.st_mode):
417 self.symlink_target = os.readlink(path)
419 add_error('readlink: %s', e)
421 def _encode_symlink_target(self):
422 return self.symlink_target
424 def _load_symlink_target_rec(self, port):
425 self.symlink_target = vint.read_bvec(port)
430 def _add_hardlink_target(self, target):
431 self.hardlink_target = target
433 def _same_hardlink_target(self, other):
434 """Return true or false to indicate similarity in the hardlink sense."""
435 return self.hardlink_target == other.hardlink_target
437 def _encode_hardlink_target(self):
438 return self.hardlink_target
440 def _load_hardlink_target_rec(self, port):
441 self.hardlink_target = vint.read_bvec(port)
444 ## POSIX1e ACL records
446 # Recorded as a list:
447 # [txt_id_acl, num_id_acl]
448 # or, if a directory:
449 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
450 # The numeric/text distinction only matters when reading/restoring
452 def _add_posix1e_acl(self, path, st):
453 if not posix1e: return
454 if not stat.S_ISLNK(st.st_mode):
456 if posix1e.has_extended(path):
457 acl = posix1e.ACL(file=path)
458 self.posix1e_acl = [acl, acl] # txt and num are the same
459 if stat.S_ISDIR(st.st_mode):
460 acl = posix1e.ACL(filedef=path)
461 self.posix1e_acl.extend([acl, acl])
462 except EnvironmentError, e:
463 if e.errno != errno.EOPNOTSUPP:
466 def _same_posix1e_acl(self, other):
467 """Return true or false to indicate similarity in the hardlink sense."""
468 return self.posix1e_acl == other.posix1e_acl
470 def _encode_posix1e_acl(self):
471 # Encode as two strings (w/default ACL string possibly empty).
473 acls = self.posix1e_acl
474 txt_flags = posix1e.TEXT_ABBREVIATE
475 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
476 acl_reps = [acls[0].to_any_text('', '\n', txt_flags),
477 acls[1].to_any_text('', '\n', num_flags)]
481 acl_reps.append(acls[2].to_any_text('', '\n', txt_flags))
482 acl_reps.append(acls[3].to_any_text('', '\n', num_flags))
483 return vint.pack('ssss',
484 acl_reps[0], acl_reps[1], acl_reps[2], acl_reps[3])
488 def _load_posix1e_acl_rec(self, port):
489 data = vint.read_bvec(port)
490 acl_reps = vint.unpack('ssss', data)
491 if acl_reps[2] == '':
492 acl_reps = acl_reps[:2]
493 self.posix1e_acl = [posix1e.ACL(text=x) for x in acl_reps]
495 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
496 def apply_acl(acl, kind):
498 acl.applyto(path, kind)
500 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
501 raise ApplyError('POSIX1e ACL applyto: %s' % e)
507 add_error("%s: can't restore ACLs; posix1e support missing.\n"
511 acls = self.posix1e_acl
513 if restore_numeric_ids:
514 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
516 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
517 if restore_numeric_ids:
518 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
520 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
523 ## Linux attributes (lsattr(1), chattr(1))
525 def _add_linux_attr(self, path, st):
526 if not get_linux_file_attr: return
527 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
529 attr = get_linux_file_attr(path)
531 self.linux_attr = attr
533 if e.errno == errno.EACCES:
534 add_error('read Linux attr: %s' % e)
535 elif e.errno == errno.ENOTTY or e.errno == errno.ENOSYS:
536 # ENOTTY: Function not implemented.
537 # ENOSYS: Inappropriate ioctl for device.
538 # Assume filesystem doesn't support attrs.
543 def _same_linux_attr(self, other):
544 """Return true or false to indicate similarity in the hardlink sense."""
545 return self.linux_attr == other.linux_attr
547 def _encode_linux_attr(self):
549 return vint.pack('V', self.linux_attr)
553 def _load_linux_attr_rec(self, port):
554 data = vint.read_bvec(port)
555 self.linux_attr = vint.unpack('V', data)[0]
557 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
559 if not set_linux_file_attr:
560 add_error("%s: can't restore linuxattrs: "
561 "linuxattr support missing.\n" % path)
564 set_linux_file_attr(path, self.linux_attr)
566 if e.errno == errno.ENOTTY:
567 raise ApplyError('Linux chattr: %s' % e)
572 ## Linux extended attributes (getfattr(1), setfattr(1))
574 def _add_linux_xattr(self, path, st):
577 self.linux_xattr = xattr.get_all(path, nofollow=True)
578 except EnvironmentError, e:
579 if e.errno != errno.EOPNOTSUPP:
582 def _same_linux_xattr(self, other):
583 """Return true or false to indicate similarity in the hardlink sense."""
584 return self.linux_xattr == other.linux_xattr
586 def _encode_linux_xattr(self):
588 result = vint.pack('V', len(self.linux_xattr))
589 for name, value in self.linux_xattr:
590 result += vint.pack('ss', name, value)
595 def _load_linux_xattr_rec(self, file):
596 data = vint.read_bvec(file)
597 memfile = StringIO(data)
599 for i in range(vint.read_vuint(memfile)):
600 key = vint.read_bvec(memfile)
601 value = vint.read_bvec(memfile)
602 result.append((key, value))
603 self.linux_xattr = result
605 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
608 add_error("%s: can't restore xattr; xattr support missing.\n"
611 existing_xattrs = set(xattr.list(path, nofollow=True))
613 for k, v in self.linux_xattr:
614 if k not in existing_xattrs \
615 or v != xattr.get(path, k, nofollow=True):
617 xattr.set(path, k, v, nofollow=True)
619 if e.errno == errno.EPERM \
620 or e.errno == errno.EOPNOTSUPP:
621 raise ApplyError('xattr.set: %s' % e)
624 existing_xattrs -= frozenset([k])
625 for k in existing_xattrs:
627 xattr.remove(path, k, nofollow=True)
629 if e.errno == errno.EPERM:
630 raise ApplyError('xattr.remove: %s' % e)
639 self.symlink_target = None
640 self.hardlink_target = None
641 self.linux_attr = None
642 self.linux_xattr = None
643 self.posix1e_acl = None
644 self.posix1e_acl_default = None
646 def write(self, port, include_path=True):
647 records = include_path and [(_rec_tag_path, self._encode_path())] or []
648 records.extend([(_rec_tag_common, self._encode_common()),
649 (_rec_tag_symlink_target,
650 self._encode_symlink_target()),
651 (_rec_tag_hardlink_target,
652 self._encode_hardlink_target()),
653 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
654 (_rec_tag_linux_attr, self._encode_linux_attr()),
655 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
656 for tag, data in records:
658 vint.write_vuint(port, tag)
659 vint.write_bvec(port, data)
660 vint.write_vuint(port, _rec_tag_end)
662 def encode(self, include_path=True):
664 self.write(port, include_path)
665 return port.getvalue()
669 # This method should either return a valid Metadata object,
670 # return None if there was no information at all (just a
671 # _rec_tag_end), throw EOFError if there was nothing at all to
672 # read, or throw an Exception if a valid object could not be
674 tag = vint.read_vuint(port)
675 if tag == _rec_tag_end:
677 try: # From here on, EOF is an error.
679 while True: # only exit is error (exception) or _rec_tag_end
680 if tag == _rec_tag_path:
681 result._load_path_rec(port)
682 elif tag == _rec_tag_common:
683 result._load_common_rec(port)
684 elif tag == _rec_tag_symlink_target:
685 result._load_symlink_target_rec(port)
686 elif tag == _rec_tag_hardlink_target:
687 result._load_hardlink_target_rec(port)
688 elif tag == _rec_tag_posix1e_acl:
689 result._load_posix1e_acl_rec(port)
690 elif tag ==_rec_tag_nfsv4_acl:
691 result._load_nfsv4_acl_rec(port)
692 elif tag == _rec_tag_linux_attr:
693 result._load_linux_attr_rec(port)
694 elif tag == _rec_tag_linux_xattr:
695 result._load_linux_xattr_rec(port)
696 elif tag == _rec_tag_end:
698 else: # unknown record
700 tag = vint.read_vuint(port)
702 raise Exception("EOF while reading Metadata")
705 return stat.S_ISDIR(self.mode)
707 def create_path(self, path, create_symlinks=True):
708 self._create_via_common_rec(path, create_symlinks=create_symlinks)
710 def apply_to_path(self, path=None, restore_numeric_ids=False):
711 # apply metadata to path -- file must exist
715 raise Exception('Metadata.apply_to_path() called with no path')
716 if not self._recognized_file_type():
717 add_error('not applying metadata to "%s"' % path
718 + ' with unrecognized mode "0x%x"\n' % self.mode)
720 num_ids = restore_numeric_ids
722 self._apply_common_rec(path, restore_numeric_ids=num_ids)
723 self._apply_posix1e_acl_rec(path, restore_numeric_ids=num_ids)
724 self._apply_linux_attr_rec(path, restore_numeric_ids=num_ids)
725 self._apply_linux_xattr_rec(path, restore_numeric_ids=num_ids)
726 except ApplyError, e:
729 def same_file(self, other):
730 """Compare this to other for equivalency. Return true if
731 their information implies they could represent the same file
732 on disk, in the hardlink sense. Assume they're both regular
734 return self._same_common(other) \
735 and self._same_hardlink_target(other) \
736 and self._same_posix1e_acl(other) \
737 and self._same_linux_attr(other) \
738 and self._same_linux_xattr(other)
741 def from_path(path, statinfo=None, archive_path=None,
742 save_symlinks=True, hardlink_target=None):
744 result.path = archive_path
745 st = statinfo or xstat.lstat(path)
746 result.size = st.st_size
747 result._add_common(path, st)
749 result._add_symlink_target(path, st)
750 result._add_hardlink_target(hardlink_target)
751 result._add_posix1e_acl(path, st)
752 result._add_linux_attr(path, st)
753 result._add_linux_xattr(path, st)
757 def save_tree(output_file, paths,
763 # Issue top-level rewrite warnings.
765 safe_path = _clean_up_path_for_archive(path)
766 if safe_path != path:
767 log('archiving "%s" as "%s"\n' % (path, safe_path))
769 start_dir = os.getcwd()
771 for (p, st) in recursive_dirlist(paths, xdev=xdev):
772 dirlist_dir = os.getcwd()
774 safe_path = _clean_up_path_for_archive(p)
775 m = from_path(p, statinfo=st, archive_path=safe_path,
776 save_symlinks=save_symlinks)
778 print >> sys.stderr, m.path
779 m.write(output_file, include_path=write_paths)
780 os.chdir(dirlist_dir)
785 def _set_up_path(meta, create_symlinks=True):
786 # Allow directories to exist as a special case -- might have
787 # been created by an earlier longer path.
791 parent = os.path.dirname(meta.path)
794 meta.create_path(meta.path, create_symlinks=create_symlinks)
797 all_fields = frozenset(['path',
814 def summary_str(meta):
815 mode_val = xstat.mode_str(meta.mode)
818 user_val = str(meta.uid)
819 group_val = meta.group
821 group_val = str(meta.gid)
822 size_or_dev_val = '-'
823 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
824 size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
826 size_or_dev_val = meta.size
827 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
828 time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
829 path_val = meta.path or ''
830 if stat.S_ISLNK(meta.mode):
831 path_val += ' -> ' + meta.symlink_target
832 return '%-10s %-11s %11s %16s %s' % (mode_val,
833 user_val + "/" + group_val,
839 def detailed_str(meta, fields = None):
840 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
841 # 0", "link-target:", etc.
847 path = meta.path or ''
848 result.append('path: ' + path)
850 result.append('mode: %s (%s)' % (oct(meta.mode),
851 xstat.mode_str(meta.mode)))
852 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
853 result.append('link-target: ' + meta.symlink_target)
856 result.append('rdev: %d,%d' % (os.major(meta.rdev),
857 os.minor(meta.rdev)))
859 result.append('rdev: 0')
860 if 'size' in fields and meta.size:
861 result.append('size: ' + str(meta.size))
863 result.append('uid: ' + str(meta.uid))
865 result.append('gid: ' + str(meta.gid))
867 result.append('user: ' + meta.user)
868 if 'group' in fields:
869 result.append('group: ' + meta.group)
870 if 'atime' in fields:
871 # If we don't have xstat.lutime, that means we have to use
872 # utime(), and utime() has no way to set the mtime/atime of a
873 # symlink. Thus, the mtime/atime of a symlink is meaningless,
874 # so let's not report it. (That way scripts comparing
875 # before/after won't trigger.)
876 if xstat.lutime or not stat.S_ISLNK(meta.mode):
877 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
879 result.append('atime: 0')
880 if 'mtime' in fields:
881 if xstat.lutime or not stat.S_ISLNK(meta.mode):
882 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
884 result.append('mtime: 0')
885 if 'ctime' in fields:
886 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
887 if 'linux-attr' in fields and meta.linux_attr:
888 result.append('linux-attr: ' + hex(meta.linux_attr))
889 if 'linux-xattr' in fields and meta.linux_xattr:
890 for name, value in meta.linux_xattr:
891 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
892 if 'posix1e-acl' in fields and meta.posix1e_acl and posix1e:
893 flags = posix1e.TEXT_ABBREVIATE
894 if stat.S_ISDIR(meta.mode):
895 acl = meta.posix1e_acl[0]
896 default_acl = meta.posix1e_acl[2]
897 result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
898 result.append(acl.to_any_text('posix1e-acl-default: ', '\n', flags))
900 acl = meta.posix1e_acl[0]
901 result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
902 return '\n'.join(result)
905 class _ArchiveIterator:
908 return Metadata.read(self._file)
910 raise StopIteration()
915 def __init__(self, file):
919 def display_archive(file):
922 for meta in _ArchiveIterator(file):
925 print detailed_str(meta)
928 for meta in _ArchiveIterator(file):
929 print summary_str(meta)
931 for meta in _ArchiveIterator(file):
933 print >> sys.stderr, \
934 'bup: no metadata path, but asked to only display path', \
935 '(increase verbosity?)'
940 def start_extract(file, create_symlinks=True):
941 for meta in _ArchiveIterator(file):
942 if not meta: # Hit end record.
945 print >> sys.stderr, meta.path
946 xpath = _clean_up_extract_path(meta.path)
948 add_error(Exception('skipping risky path "%s"' % meta.path))
951 _set_up_path(meta, create_symlinks=create_symlinks)
954 def finish_extract(file, restore_numeric_ids=False):
956 for meta in _ArchiveIterator(file):
957 if not meta: # Hit end record.
959 xpath = _clean_up_extract_path(meta.path)
961 add_error(Exception('skipping risky path "%s"' % dir.path))
963 if os.path.isdir(meta.path):
964 all_dirs.append(meta)
967 print >> sys.stderr, meta.path
968 meta.apply_to_path(path=xpath,
969 restore_numeric_ids=restore_numeric_ids)
970 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
972 # Don't need to check xpath -- won't be in all_dirs if not OK.
973 xpath = _clean_up_extract_path(dir.path)
975 print >> sys.stderr, dir.path
976 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
979 def extract(file, restore_numeric_ids=False, create_symlinks=True):
980 # For now, just store all the directories and handle them last,
983 for meta in _ArchiveIterator(file):
984 if not meta: # Hit end record.
986 xpath = _clean_up_extract_path(meta.path)
988 add_error(Exception('skipping risky path "%s"' % meta.path))
992 print >> sys.stderr, '+', meta.path
993 _set_up_path(meta, create_symlinks=create_symlinks)
994 if os.path.isdir(meta.path):
995 all_dirs.append(meta)
998 print >> sys.stderr, '=', meta.path
999 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1000 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1001 for dir in all_dirs:
1002 # Don't need to check xpath -- won't be in all_dirs if not OK.
1003 xpath = _clean_up_extract_path(dir.path)
1005 print >> sys.stderr, '=', xpath
1006 # Shouldn't have to check for risky paths here (omitted above).
1007 dir.apply_to_path(path=dir.path,
1008 restore_numeric_ids=restore_numeric_ids)