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, socket
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') or sys.platform.startswith('darwin')):
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 in (errno.ENOTEMPTY, errno.EEXIST):
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):
316 os.mknod(path, 0600 | stat.S_IFSOCK)
318 if e.errno == errno.EINVAL:
319 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
323 elif stat.S_ISLNK(self.mode):
324 assert(self._recognized_file_type())
325 if self.symlink_target and create_symlinks:
326 # on MacOS, symlink() permissions depend on umask, and there's
327 # no way to chown a symlink after creating it, so we have to
329 oldumask = os.umask((self.mode & 0777) ^ 0777)
331 os.symlink(self.symlink_target, path)
334 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
336 assert(not self._recognized_file_type())
337 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
340 def _apply_common_rec(self, path, restore_numeric_ids=False):
342 raise ApplyError('no metadata - cannot apply to ' + path)
344 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
345 # EACCES errors at this stage are fatal for the current path.
346 if lutime and stat.S_ISLNK(self.mode):
348 lutime(path, (self.atime, self.mtime))
350 if e.errno == errno.EACCES:
351 raise ApplyError('lutime: %s' % e)
356 utime(path, (self.atime, self.mtime))
358 if e.errno == errno.EACCES:
359 raise ApplyError('utime: %s' % e)
363 # Implement tar/rsync-like semantics; see bup-restore(1).
364 # FIXME: should we consider caching user/group name <-> id
365 # mappings, getgroups(), etc.?
366 uid = gid = -1 # By default, do nothing.
370 if not restore_numeric_ids:
371 if self.uid != 0 and self.user:
372 entry = pwd_from_name(self.user)
375 if self.gid != 0 and self.group:
376 entry = grp_from_name(self.group)
379 else: # not superuser - only consider changing the group/gid
380 user_gids = os.getgroups()
381 if self.gid in user_gids:
383 if not restore_numeric_ids and self.gid != 0:
384 # The grp might not exist on the local system.
385 grps = filter(None, [grp_from_gid(x) for x in user_gids])
386 if self.group in [x.gr_name for x in grps]:
387 g = grp_from_name(self.group)
391 if uid != -1 or gid != -1:
393 os.lchown(path, uid, gid)
395 if e.errno == errno.EPERM:
396 add_error('lchown: %s' % e)
397 elif sys.platform.startswith('cygwin') \
398 and e.errno == errno.EINVAL:
399 add_error('lchown: unknown uid/gid (%d/%d) for %s'
405 os.lchmod(path, stat.S_IMODE(self.mode))
406 elif not stat.S_ISLNK(self.mode):
407 os.chmod(path, stat.S_IMODE(self.mode))
412 def _encode_path(self):
414 return vint.pack('s', self.path)
418 def _load_path_rec(self, port):
419 self.path = vint.unpack('s', vint.read_bvec(port))[0]
424 def _add_symlink_target(self, path, st):
426 if stat.S_ISLNK(st.st_mode):
427 self.symlink_target = os.readlink(path)
429 add_error('readlink: %s', e)
431 def _encode_symlink_target(self):
432 return self.symlink_target
434 def _load_symlink_target_rec(self, port):
435 self.symlink_target = vint.read_bvec(port)
440 def _add_hardlink_target(self, target):
441 self.hardlink_target = target
443 def _same_hardlink_target(self, other):
444 """Return true or false to indicate similarity in the hardlink sense."""
445 return self.hardlink_target == other.hardlink_target
447 def _encode_hardlink_target(self):
448 return self.hardlink_target
450 def _load_hardlink_target_rec(self, port):
451 self.hardlink_target = vint.read_bvec(port)
454 ## POSIX1e ACL records
456 # Recorded as a list:
457 # [txt_id_acl, num_id_acl]
458 # or, if a directory:
459 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
460 # The numeric/text distinction only matters when reading/restoring
462 def _add_posix1e_acl(self, path, st):
463 if not posix1e: return
464 if not stat.S_ISLNK(st.st_mode):
466 if posix1e.has_extended(path):
467 acl = posix1e.ACL(file=path)
468 self.posix1e_acl = [acl, acl] # txt and num are the same
469 if stat.S_ISDIR(st.st_mode):
470 acl = posix1e.ACL(filedef=path)
471 self.posix1e_acl.extend([acl, acl])
472 except EnvironmentError, e:
473 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
476 def _same_posix1e_acl(self, other):
477 """Return true or false to indicate similarity in the hardlink sense."""
478 return self.posix1e_acl == other.posix1e_acl
480 def _encode_posix1e_acl(self):
481 # Encode as two strings (w/default ACL string possibly empty).
483 acls = self.posix1e_acl
484 txt_flags = posix1e.TEXT_ABBREVIATE
485 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
486 acl_reps = [acls[0].to_any_text('', '\n', txt_flags),
487 acls[1].to_any_text('', '\n', num_flags)]
491 acl_reps.append(acls[2].to_any_text('', '\n', txt_flags))
492 acl_reps.append(acls[3].to_any_text('', '\n', num_flags))
493 return vint.pack('ssss',
494 acl_reps[0], acl_reps[1], acl_reps[2], acl_reps[3])
498 def _load_posix1e_acl_rec(self, port):
499 if not posix1e: return
500 data = vint.read_bvec(port)
501 acl_reps = vint.unpack('ssss', data)
502 if acl_reps[2] == '':
503 acl_reps = acl_reps[:2]
504 self.posix1e_acl = [posix1e.ACL(text=x) for x in acl_reps]
506 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
507 def apply_acl(acl, kind):
509 acl.applyto(path, kind)
511 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
512 raise ApplyError('POSIX1e ACL applyto: %s' % e)
518 add_error("%s: can't restore ACLs; posix1e support missing.\n"
522 acls = self.posix1e_acl
524 if restore_numeric_ids:
525 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
527 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
528 if restore_numeric_ids:
529 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
531 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
534 ## Linux attributes (lsattr(1), chattr(1))
536 def _add_linux_attr(self, path, st):
537 if not get_linux_file_attr: return
538 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
540 attr = get_linux_file_attr(path)
542 self.linux_attr = attr
544 if e.errno == errno.EACCES:
545 add_error('read Linux attr: %s' % e)
546 elif e.errno == errno.ENOTTY or e.errno == errno.ENOSYS:
547 # ENOTTY: Function not implemented.
548 # ENOSYS: Inappropriate ioctl for device.
549 # Assume filesystem doesn't support attrs.
554 def _same_linux_attr(self, other):
555 """Return true or false to indicate similarity in the hardlink sense."""
556 return self.linux_attr == other.linux_attr
558 def _encode_linux_attr(self):
560 return vint.pack('V', self.linux_attr)
564 def _load_linux_attr_rec(self, port):
565 data = vint.read_bvec(port)
566 self.linux_attr = vint.unpack('V', data)[0]
568 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
570 if not set_linux_file_attr:
571 add_error("%s: can't restore linuxattrs: "
572 "linuxattr support missing.\n" % path)
575 set_linux_file_attr(path, self.linux_attr)
577 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS):
578 raise ApplyError('Linux chattr: %s' % e)
583 ## Linux extended attributes (getfattr(1), setfattr(1))
585 def _add_linux_xattr(self, path, st):
588 self.linux_xattr = xattr.get_all(path, nofollow=True)
589 except EnvironmentError, e:
590 if e.errno != errno.EOPNOTSUPP:
593 def _same_linux_xattr(self, other):
594 """Return true or false to indicate similarity in the hardlink sense."""
595 return self.linux_xattr == other.linux_xattr
597 def _encode_linux_xattr(self):
599 result = vint.pack('V', len(self.linux_xattr))
600 for name, value in self.linux_xattr:
601 result += vint.pack('ss', name, value)
606 def _load_linux_xattr_rec(self, file):
607 data = vint.read_bvec(file)
608 memfile = StringIO(data)
610 for i in range(vint.read_vuint(memfile)):
611 key = vint.read_bvec(memfile)
612 value = vint.read_bvec(memfile)
613 result.append((key, value))
614 self.linux_xattr = result
616 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
619 add_error("%s: can't restore xattr; xattr support missing.\n"
622 existing_xattrs = set(xattr.list(path, nofollow=True))
624 for k, v in self.linux_xattr:
625 if k not in existing_xattrs \
626 or v != xattr.get(path, k, nofollow=True):
628 xattr.set(path, k, v, nofollow=True)
630 if e.errno == errno.EPERM \
631 or e.errno == errno.EOPNOTSUPP:
632 raise ApplyError('xattr.set: %s' % e)
635 existing_xattrs -= frozenset([k])
636 for k in existing_xattrs:
638 xattr.remove(path, k, nofollow=True)
640 if e.errno == errno.EPERM:
641 raise ApplyError('xattr.remove: %s' % e)
650 self.symlink_target = None
651 self.hardlink_target = None
652 self.linux_attr = None
653 self.linux_xattr = None
654 self.posix1e_acl = None
655 self.posix1e_acl_default = None
657 def write(self, port, include_path=True):
658 records = include_path and [(_rec_tag_path, self._encode_path())] or []
659 records.extend([(_rec_tag_common, self._encode_common()),
660 (_rec_tag_symlink_target,
661 self._encode_symlink_target()),
662 (_rec_tag_hardlink_target,
663 self._encode_hardlink_target()),
664 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
665 (_rec_tag_linux_attr, self._encode_linux_attr()),
666 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
667 for tag, data in records:
669 vint.write_vuint(port, tag)
670 vint.write_bvec(port, data)
671 vint.write_vuint(port, _rec_tag_end)
673 def encode(self, include_path=True):
675 self.write(port, include_path)
676 return port.getvalue()
680 # This method should either return a valid Metadata object,
681 # return None if there was no information at all (just a
682 # _rec_tag_end), throw EOFError if there was nothing at all to
683 # read, or throw an Exception if a valid object could not be
685 tag = vint.read_vuint(port)
686 if tag == _rec_tag_end:
688 try: # From here on, EOF is an error.
690 while True: # only exit is error (exception) or _rec_tag_end
691 if tag == _rec_tag_path:
692 result._load_path_rec(port)
693 elif tag == _rec_tag_common:
694 result._load_common_rec(port)
695 elif tag == _rec_tag_symlink_target:
696 result._load_symlink_target_rec(port)
697 elif tag == _rec_tag_hardlink_target:
698 result._load_hardlink_target_rec(port)
699 elif tag == _rec_tag_posix1e_acl:
700 result._load_posix1e_acl_rec(port)
701 elif tag == _rec_tag_nfsv4_acl:
702 result._load_nfsv4_acl_rec(port)
703 elif tag == _rec_tag_linux_attr:
704 result._load_linux_attr_rec(port)
705 elif tag == _rec_tag_linux_xattr:
706 result._load_linux_xattr_rec(port)
707 elif tag == _rec_tag_end:
709 else: # unknown record
711 tag = vint.read_vuint(port)
713 raise Exception("EOF while reading Metadata")
716 return stat.S_ISDIR(self.mode)
718 def create_path(self, path, create_symlinks=True):
719 self._create_via_common_rec(path, create_symlinks=create_symlinks)
721 def apply_to_path(self, path=None, restore_numeric_ids=False):
722 # apply metadata to path -- file must exist
726 raise Exception('Metadata.apply_to_path() called with no path')
727 if not self._recognized_file_type():
728 add_error('not applying metadata to "%s"' % path
729 + ' with unrecognized mode "0x%x"\n' % self.mode)
731 num_ids = restore_numeric_ids
733 self._apply_common_rec(path, restore_numeric_ids=num_ids)
734 self._apply_posix1e_acl_rec(path, restore_numeric_ids=num_ids)
735 self._apply_linux_attr_rec(path, restore_numeric_ids=num_ids)
736 self._apply_linux_xattr_rec(path, restore_numeric_ids=num_ids)
737 except ApplyError, e:
740 def same_file(self, other):
741 """Compare this to other for equivalency. Return true if
742 their information implies they could represent the same file
743 on disk, in the hardlink sense. Assume they're both regular
745 return self._same_common(other) \
746 and self._same_hardlink_target(other) \
747 and self._same_posix1e_acl(other) \
748 and self._same_linux_attr(other) \
749 and self._same_linux_xattr(other)
752 def from_path(path, statinfo=None, archive_path=None,
753 save_symlinks=True, hardlink_target=None):
755 result.path = archive_path
756 st = statinfo or xstat.lstat(path)
757 result.size = st.st_size
758 result._add_common(path, st)
760 result._add_symlink_target(path, st)
761 result._add_hardlink_target(hardlink_target)
762 result._add_posix1e_acl(path, st)
763 result._add_linux_attr(path, st)
764 result._add_linux_xattr(path, st)
768 def save_tree(output_file, paths,
774 # Issue top-level rewrite warnings.
776 safe_path = _clean_up_path_for_archive(path)
777 if safe_path != path:
778 log('archiving "%s" as "%s"\n' % (path, safe_path))
780 start_dir = os.getcwd()
782 for (p, st) in recursive_dirlist(paths, xdev=xdev):
783 dirlist_dir = os.getcwd()
785 safe_path = _clean_up_path_for_archive(p)
786 m = from_path(p, statinfo=st, archive_path=safe_path,
787 save_symlinks=save_symlinks)
789 print >> sys.stderr, m.path
790 m.write(output_file, include_path=write_paths)
791 os.chdir(dirlist_dir)
796 def _set_up_path(meta, create_symlinks=True):
797 # Allow directories to exist as a special case -- might have
798 # been created by an earlier longer path.
802 parent = os.path.dirname(meta.path)
805 meta.create_path(meta.path, create_symlinks=create_symlinks)
808 all_fields = frozenset(['path',
825 def summary_str(meta):
826 mode_val = xstat.mode_str(meta.mode)
829 user_val = str(meta.uid)
830 group_val = meta.group
832 group_val = str(meta.gid)
833 size_or_dev_val = '-'
834 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
835 size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
837 size_or_dev_val = meta.size
838 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
839 time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
840 path_val = meta.path or ''
841 if stat.S_ISLNK(meta.mode):
842 path_val += ' -> ' + meta.symlink_target
843 return '%-10s %-11s %11s %16s %s' % (mode_val,
844 user_val + "/" + group_val,
850 def detailed_str(meta, fields = None):
851 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
852 # 0", "link-target:", etc.
858 path = meta.path or ''
859 result.append('path: ' + path)
861 result.append('mode: %s (%s)' % (oct(meta.mode),
862 xstat.mode_str(meta.mode)))
863 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
864 result.append('link-target: ' + meta.symlink_target)
867 result.append('rdev: %d,%d' % (os.major(meta.rdev),
868 os.minor(meta.rdev)))
870 result.append('rdev: 0')
871 if 'size' in fields and meta.size:
872 result.append('size: ' + str(meta.size))
874 result.append('uid: ' + str(meta.uid))
876 result.append('gid: ' + str(meta.gid))
878 result.append('user: ' + meta.user)
879 if 'group' in fields:
880 result.append('group: ' + meta.group)
881 if 'atime' in fields:
882 # If we don't have xstat.lutime, that means we have to use
883 # utime(), and utime() has no way to set the mtime/atime of a
884 # symlink. Thus, the mtime/atime of a symlink is meaningless,
885 # so let's not report it. (That way scripts comparing
886 # before/after won't trigger.)
887 if xstat.lutime or not stat.S_ISLNK(meta.mode):
888 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
890 result.append('atime: 0')
891 if 'mtime' in fields:
892 if xstat.lutime or not stat.S_ISLNK(meta.mode):
893 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
895 result.append('mtime: 0')
896 if 'ctime' in fields:
897 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
898 if 'linux-attr' in fields and meta.linux_attr:
899 result.append('linux-attr: ' + hex(meta.linux_attr))
900 if 'linux-xattr' in fields and meta.linux_xattr:
901 for name, value in meta.linux_xattr:
902 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
903 if 'posix1e-acl' in fields and meta.posix1e_acl and posix1e:
904 flags = posix1e.TEXT_ABBREVIATE
905 if stat.S_ISDIR(meta.mode):
906 acl = meta.posix1e_acl[0]
907 default_acl = meta.posix1e_acl[2]
908 result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
909 result.append(acl.to_any_text('posix1e-acl-default: ', '\n', flags))
911 acl = meta.posix1e_acl[0]
912 result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
913 return '\n'.join(result)
916 class _ArchiveIterator:
919 return Metadata.read(self._file)
921 raise StopIteration()
926 def __init__(self, file):
930 def display_archive(file):
933 for meta in _ArchiveIterator(file):
936 print detailed_str(meta)
939 for meta in _ArchiveIterator(file):
940 print summary_str(meta)
942 for meta in _ArchiveIterator(file):
944 print >> sys.stderr, \
945 'bup: no metadata path, but asked to only display path', \
946 '(increase verbosity?)'
951 def start_extract(file, create_symlinks=True):
952 for meta in _ArchiveIterator(file):
953 if not meta: # Hit end record.
956 print >> sys.stderr, meta.path
957 xpath = _clean_up_extract_path(meta.path)
959 add_error(Exception('skipping risky path "%s"' % meta.path))
962 _set_up_path(meta, create_symlinks=create_symlinks)
965 def finish_extract(file, restore_numeric_ids=False):
967 for meta in _ArchiveIterator(file):
968 if not meta: # Hit end record.
970 xpath = _clean_up_extract_path(meta.path)
972 add_error(Exception('skipping risky path "%s"' % dir.path))
974 if os.path.isdir(meta.path):
975 all_dirs.append(meta)
978 print >> sys.stderr, meta.path
979 meta.apply_to_path(path=xpath,
980 restore_numeric_ids=restore_numeric_ids)
981 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
983 # Don't need to check xpath -- won't be in all_dirs if not OK.
984 xpath = _clean_up_extract_path(dir.path)
986 print >> sys.stderr, dir.path
987 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
990 def extract(file, restore_numeric_ids=False, create_symlinks=True):
991 # For now, just store all the directories and handle them last,
994 for meta in _ArchiveIterator(file):
995 if not meta: # Hit end record.
997 xpath = _clean_up_extract_path(meta.path)
999 add_error(Exception('skipping risky path "%s"' % meta.path))
1003 print >> sys.stderr, '+', meta.path
1004 _set_up_path(meta, create_symlinks=create_symlinks)
1005 if os.path.isdir(meta.path):
1006 all_dirs.append(meta)
1009 print >> sys.stderr, '=', meta.path
1010 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1011 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1012 for dir in all_dirs:
1013 # Don't need to check xpath -- won't be in all_dirs if not OK.
1014 xpath = _clean_up_extract_path(dir.path)
1016 print >> sys.stderr, '=', xpath
1017 # Shouldn't have to check for risky paths here (omitted above).
1018 dir.apply_to_path(path=dir.path,
1019 restore_numeric_ids=restore_numeric_ids)