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 == 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 if not sys.platform.startswith('cygwin'):
316 os.mknod(path, 0600 | stat.S_IFSOCK)
318 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
320 elif stat.S_ISLNK(self.mode):
321 assert(self._recognized_file_type())
322 if self.symlink_target and create_symlinks:
323 # on MacOS, symlink() permissions depend on umask, and there's
324 # no way to chown a symlink after creating it, so we have to
326 oldumask = os.umask((self.mode & 0777) ^ 0777)
328 os.symlink(self.symlink_target, path)
331 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
333 assert(not self._recognized_file_type())
334 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
337 def _apply_common_rec(self, path, restore_numeric_ids=False):
339 raise ApplyError('no metadata - cannot apply to ' + path)
341 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
342 # EACCES errors at this stage are fatal for the current path.
343 if lutime and stat.S_ISLNK(self.mode):
345 lutime(path, (self.atime, self.mtime))
347 if e.errno == errno.EACCES:
348 raise ApplyError('lutime: %s' % e)
353 utime(path, (self.atime, self.mtime))
355 if e.errno == errno.EACCES:
356 raise ApplyError('utime: %s' % e)
360 # Implement tar/rsync-like semantics; see bup-restore(1).
361 # FIXME: should we consider caching user/group name <-> id
362 # mappings, getgroups(), etc.?
363 uid = gid = -1 # By default, do nothing.
367 if not restore_numeric_ids:
368 if self.uid != 0 and self.user:
369 entry = pwd_from_name(self.user)
372 if self.gid != 0 and self.group:
373 entry = grp_from_name(self.group)
376 else: # not superuser - only consider changing the group/gid
377 user_gids = os.getgroups()
378 if self.gid in user_gids:
380 if not restore_numeric_ids and self.gid != 0:
381 # The grp might not exist on the local system.
382 grps = filter(None, [grp_from_gid(x) for x in user_gids])
383 if self.group in [x.gr_name for x in grps]:
384 g = grp_from_name(self.group)
388 if uid != -1 or gid != -1:
390 os.lchown(path, uid, gid)
392 if e.errno == errno.EPERM:
393 add_error('lchown: %s' % e)
398 os.lchmod(path, stat.S_IMODE(self.mode))
399 elif not stat.S_ISLNK(self.mode):
400 os.chmod(path, stat.S_IMODE(self.mode))
405 def _encode_path(self):
407 return vint.pack('s', self.path)
411 def _load_path_rec(self, port):
412 self.path = vint.unpack('s', vint.read_bvec(port))[0]
417 def _add_symlink_target(self, path, st):
419 if stat.S_ISLNK(st.st_mode):
420 self.symlink_target = os.readlink(path)
422 add_error('readlink: %s', e)
424 def _encode_symlink_target(self):
425 return self.symlink_target
427 def _load_symlink_target_rec(self, port):
428 self.symlink_target = vint.read_bvec(port)
433 def _add_hardlink_target(self, target):
434 self.hardlink_target = target
436 def _same_hardlink_target(self, other):
437 """Return true or false to indicate similarity in the hardlink sense."""
438 return self.hardlink_target == other.hardlink_target
440 def _encode_hardlink_target(self):
441 return self.hardlink_target
443 def _load_hardlink_target_rec(self, port):
444 self.hardlink_target = vint.read_bvec(port)
447 ## POSIX1e ACL records
449 # Recorded as a list:
450 # [txt_id_acl, num_id_acl]
451 # or, if a directory:
452 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
453 # The numeric/text distinction only matters when reading/restoring
455 def _add_posix1e_acl(self, path, st):
456 if not posix1e: return
457 if not stat.S_ISLNK(st.st_mode):
459 if posix1e.has_extended(path):
460 acl = posix1e.ACL(file=path)
461 self.posix1e_acl = [acl, acl] # txt and num are the same
462 if stat.S_ISDIR(st.st_mode):
463 acl = posix1e.ACL(filedef=path)
464 self.posix1e_acl.extend([acl, acl])
465 except EnvironmentError, e:
466 if e.errno != errno.EOPNOTSUPP:
469 def _same_posix1e_acl(self, other):
470 """Return true or false to indicate similarity in the hardlink sense."""
471 return self.posix1e_acl == other.posix1e_acl
473 def _encode_posix1e_acl(self):
474 # Encode as two strings (w/default ACL string possibly empty).
476 acls = self.posix1e_acl
477 txt_flags = posix1e.TEXT_ABBREVIATE
478 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
479 acl_reps = [acls[0].to_any_text('', '\n', txt_flags),
480 acls[1].to_any_text('', '\n', num_flags)]
484 acl_reps.append(acls[2].to_any_text('', '\n', txt_flags))
485 acl_reps.append(acls[3].to_any_text('', '\n', num_flags))
486 return vint.pack('ssss',
487 acl_reps[0], acl_reps[1], acl_reps[2], acl_reps[3])
491 def _load_posix1e_acl_rec(self, port):
492 data = vint.read_bvec(port)
493 acl_reps = vint.unpack('ssss', data)
494 if acl_reps[2] == '':
495 acl_reps = acl_reps[:2]
496 self.posix1e_acl = [posix1e.ACL(text=x) for x in acl_reps]
498 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
499 def apply_acl(acl, kind):
501 acl.applyto(path, kind)
503 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
504 raise ApplyError('POSIX1e ACL applyto: %s' % e)
510 add_error("%s: can't restore ACLs; posix1e support missing.\n"
514 acls = self.posix1e_acl
516 if restore_numeric_ids:
517 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
519 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
520 if restore_numeric_ids:
521 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
523 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
526 ## Linux attributes (lsattr(1), chattr(1))
528 def _add_linux_attr(self, path, st):
529 if not get_linux_file_attr: return
530 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
532 attr = get_linux_file_attr(path)
534 self.linux_attr = attr
536 if e.errno == errno.EACCES:
537 add_error('read Linux attr: %s' % e)
538 elif e.errno == errno.ENOTTY or e.errno == errno.ENOSYS:
539 # ENOTTY: Function not implemented.
540 # ENOSYS: Inappropriate ioctl for device.
541 # Assume filesystem doesn't support attrs.
546 def _same_linux_attr(self, other):
547 """Return true or false to indicate similarity in the hardlink sense."""
548 return self.linux_attr == other.linux_attr
550 def _encode_linux_attr(self):
552 return vint.pack('V', self.linux_attr)
556 def _load_linux_attr_rec(self, port):
557 data = vint.read_bvec(port)
558 self.linux_attr = vint.unpack('V', data)[0]
560 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
562 if not set_linux_file_attr:
563 add_error("%s: can't restore linuxattrs: "
564 "linuxattr support missing.\n" % path)
567 set_linux_file_attr(path, self.linux_attr)
569 if e.errno == errno.ENOTTY or e.errno == errno.EOPNOTSUPP:
570 raise ApplyError('Linux chattr: %s' % e)
575 ## Linux extended attributes (getfattr(1), setfattr(1))
577 def _add_linux_xattr(self, path, st):
580 self.linux_xattr = xattr.get_all(path, nofollow=True)
581 except EnvironmentError, e:
582 if e.errno != errno.EOPNOTSUPP:
585 def _same_linux_xattr(self, other):
586 """Return true or false to indicate similarity in the hardlink sense."""
587 return self.linux_xattr == other.linux_xattr
589 def _encode_linux_xattr(self):
591 result = vint.pack('V', len(self.linux_xattr))
592 for name, value in self.linux_xattr:
593 result += vint.pack('ss', name, value)
598 def _load_linux_xattr_rec(self, file):
599 data = vint.read_bvec(file)
600 memfile = StringIO(data)
602 for i in range(vint.read_vuint(memfile)):
603 key = vint.read_bvec(memfile)
604 value = vint.read_bvec(memfile)
605 result.append((key, value))
606 self.linux_xattr = result
608 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
611 add_error("%s: can't restore xattr; xattr support missing.\n"
614 existing_xattrs = set(xattr.list(path, nofollow=True))
616 for k, v in self.linux_xattr:
617 if k not in existing_xattrs \
618 or v != xattr.get(path, k, nofollow=True):
620 xattr.set(path, k, v, nofollow=True)
622 if e.errno == errno.EPERM \
623 or e.errno == errno.EOPNOTSUPP:
624 raise ApplyError('xattr.set: %s' % e)
627 existing_xattrs -= frozenset([k])
628 for k in existing_xattrs:
630 xattr.remove(path, k, nofollow=True)
632 if e.errno == errno.EPERM:
633 raise ApplyError('xattr.remove: %s' % e)
642 self.symlink_target = None
643 self.hardlink_target = None
644 self.linux_attr = None
645 self.linux_xattr = None
646 self.posix1e_acl = None
647 self.posix1e_acl_default = None
649 def write(self, port, include_path=True):
650 records = include_path and [(_rec_tag_path, self._encode_path())] or []
651 records.extend([(_rec_tag_common, self._encode_common()),
652 (_rec_tag_symlink_target,
653 self._encode_symlink_target()),
654 (_rec_tag_hardlink_target,
655 self._encode_hardlink_target()),
656 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
657 (_rec_tag_linux_attr, self._encode_linux_attr()),
658 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
659 for tag, data in records:
661 vint.write_vuint(port, tag)
662 vint.write_bvec(port, data)
663 vint.write_vuint(port, _rec_tag_end)
665 def encode(self, include_path=True):
667 self.write(port, include_path)
668 return port.getvalue()
672 # This method should either return a valid Metadata object,
673 # return None if there was no information at all (just a
674 # _rec_tag_end), throw EOFError if there was nothing at all to
675 # read, or throw an Exception if a valid object could not be
677 tag = vint.read_vuint(port)
678 if tag == _rec_tag_end:
680 try: # From here on, EOF is an error.
682 while True: # only exit is error (exception) or _rec_tag_end
683 if tag == _rec_tag_path:
684 result._load_path_rec(port)
685 elif tag == _rec_tag_common:
686 result._load_common_rec(port)
687 elif tag == _rec_tag_symlink_target:
688 result._load_symlink_target_rec(port)
689 elif tag == _rec_tag_hardlink_target:
690 result._load_hardlink_target_rec(port)
691 elif tag == _rec_tag_posix1e_acl:
692 result._load_posix1e_acl_rec(port)
693 elif tag ==_rec_tag_nfsv4_acl:
694 result._load_nfsv4_acl_rec(port)
695 elif tag == _rec_tag_linux_attr:
696 result._load_linux_attr_rec(port)
697 elif tag == _rec_tag_linux_xattr:
698 result._load_linux_xattr_rec(port)
699 elif tag == _rec_tag_end:
701 else: # unknown record
703 tag = vint.read_vuint(port)
705 raise Exception("EOF while reading Metadata")
708 return stat.S_ISDIR(self.mode)
710 def create_path(self, path, create_symlinks=True):
711 self._create_via_common_rec(path, create_symlinks=create_symlinks)
713 def apply_to_path(self, path=None, restore_numeric_ids=False):
714 # apply metadata to path -- file must exist
718 raise Exception('Metadata.apply_to_path() called with no path')
719 if not self._recognized_file_type():
720 add_error('not applying metadata to "%s"' % path
721 + ' with unrecognized mode "0x%x"\n' % self.mode)
723 num_ids = restore_numeric_ids
725 self._apply_common_rec(path, restore_numeric_ids=num_ids)
726 self._apply_posix1e_acl_rec(path, restore_numeric_ids=num_ids)
727 self._apply_linux_attr_rec(path, restore_numeric_ids=num_ids)
728 self._apply_linux_xattr_rec(path, restore_numeric_ids=num_ids)
729 except ApplyError, e:
732 def same_file(self, other):
733 """Compare this to other for equivalency. Return true if
734 their information implies they could represent the same file
735 on disk, in the hardlink sense. Assume they're both regular
737 return self._same_common(other) \
738 and self._same_hardlink_target(other) \
739 and self._same_posix1e_acl(other) \
740 and self._same_linux_attr(other) \
741 and self._same_linux_xattr(other)
744 def from_path(path, statinfo=None, archive_path=None,
745 save_symlinks=True, hardlink_target=None):
747 result.path = archive_path
748 st = statinfo or xstat.lstat(path)
749 result.size = st.st_size
750 result._add_common(path, st)
752 result._add_symlink_target(path, st)
753 result._add_hardlink_target(hardlink_target)
754 result._add_posix1e_acl(path, st)
755 result._add_linux_attr(path, st)
756 result._add_linux_xattr(path, st)
760 def save_tree(output_file, paths,
766 # Issue top-level rewrite warnings.
768 safe_path = _clean_up_path_for_archive(path)
769 if safe_path != path:
770 log('archiving "%s" as "%s"\n' % (path, safe_path))
772 start_dir = os.getcwd()
774 for (p, st) in recursive_dirlist(paths, xdev=xdev):
775 dirlist_dir = os.getcwd()
777 safe_path = _clean_up_path_for_archive(p)
778 m = from_path(p, statinfo=st, archive_path=safe_path,
779 save_symlinks=save_symlinks)
781 print >> sys.stderr, m.path
782 m.write(output_file, include_path=write_paths)
783 os.chdir(dirlist_dir)
788 def _set_up_path(meta, create_symlinks=True):
789 # Allow directories to exist as a special case -- might have
790 # been created by an earlier longer path.
794 parent = os.path.dirname(meta.path)
797 meta.create_path(meta.path, create_symlinks=create_symlinks)
800 all_fields = frozenset(['path',
817 def summary_str(meta):
818 mode_val = xstat.mode_str(meta.mode)
821 user_val = str(meta.uid)
822 group_val = meta.group
824 group_val = str(meta.gid)
825 size_or_dev_val = '-'
826 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
827 size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
829 size_or_dev_val = meta.size
830 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
831 time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
832 path_val = meta.path or ''
833 if stat.S_ISLNK(meta.mode):
834 path_val += ' -> ' + meta.symlink_target
835 return '%-10s %-11s %11s %16s %s' % (mode_val,
836 user_val + "/" + group_val,
842 def detailed_str(meta, fields = None):
843 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
844 # 0", "link-target:", etc.
850 path = meta.path or ''
851 result.append('path: ' + path)
853 result.append('mode: %s (%s)' % (oct(meta.mode),
854 xstat.mode_str(meta.mode)))
855 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
856 result.append('link-target: ' + meta.symlink_target)
859 result.append('rdev: %d,%d' % (os.major(meta.rdev),
860 os.minor(meta.rdev)))
862 result.append('rdev: 0')
863 if 'size' in fields and meta.size:
864 result.append('size: ' + str(meta.size))
866 result.append('uid: ' + str(meta.uid))
868 result.append('gid: ' + str(meta.gid))
870 result.append('user: ' + meta.user)
871 if 'group' in fields:
872 result.append('group: ' + meta.group)
873 if 'atime' in fields:
874 # If we don't have xstat.lutime, that means we have to use
875 # utime(), and utime() has no way to set the mtime/atime of a
876 # symlink. Thus, the mtime/atime of a symlink is meaningless,
877 # so let's not report it. (That way scripts comparing
878 # before/after won't trigger.)
879 if xstat.lutime or not stat.S_ISLNK(meta.mode):
880 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
882 result.append('atime: 0')
883 if 'mtime' in fields:
884 if xstat.lutime or not stat.S_ISLNK(meta.mode):
885 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
887 result.append('mtime: 0')
888 if 'ctime' in fields:
889 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
890 if 'linux-attr' in fields and meta.linux_attr:
891 result.append('linux-attr: ' + hex(meta.linux_attr))
892 if 'linux-xattr' in fields and meta.linux_xattr:
893 for name, value in meta.linux_xattr:
894 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
895 if 'posix1e-acl' in fields and meta.posix1e_acl and posix1e:
896 flags = posix1e.TEXT_ABBREVIATE
897 if stat.S_ISDIR(meta.mode):
898 acl = meta.posix1e_acl[0]
899 default_acl = meta.posix1e_acl[2]
900 result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
901 result.append(acl.to_any_text('posix1e-acl-default: ', '\n', flags))
903 acl = meta.posix1e_acl[0]
904 result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
905 return '\n'.join(result)
908 class _ArchiveIterator:
911 return Metadata.read(self._file)
913 raise StopIteration()
918 def __init__(self, file):
922 def display_archive(file):
925 for meta in _ArchiveIterator(file):
928 print detailed_str(meta)
931 for meta in _ArchiveIterator(file):
932 print summary_str(meta)
934 for meta in _ArchiveIterator(file):
936 print >> sys.stderr, \
937 'bup: no metadata path, but asked to only display path', \
938 '(increase verbosity?)'
943 def start_extract(file, create_symlinks=True):
944 for meta in _ArchiveIterator(file):
945 if not meta: # Hit end record.
948 print >> sys.stderr, meta.path
949 xpath = _clean_up_extract_path(meta.path)
951 add_error(Exception('skipping risky path "%s"' % meta.path))
954 _set_up_path(meta, create_symlinks=create_symlinks)
957 def finish_extract(file, restore_numeric_ids=False):
959 for meta in _ArchiveIterator(file):
960 if not meta: # Hit end record.
962 xpath = _clean_up_extract_path(meta.path)
964 add_error(Exception('skipping risky path "%s"' % dir.path))
966 if os.path.isdir(meta.path):
967 all_dirs.append(meta)
970 print >> sys.stderr, meta.path
971 meta.apply_to_path(path=xpath,
972 restore_numeric_ids=restore_numeric_ids)
973 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
975 # Don't need to check xpath -- won't be in all_dirs if not OK.
976 xpath = _clean_up_extract_path(dir.path)
978 print >> sys.stderr, dir.path
979 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
982 def extract(file, restore_numeric_ids=False, create_symlinks=True):
983 # For now, just store all the directories and handle them last,
986 for meta in _ArchiveIterator(file):
987 if not meta: # Hit end record.
989 xpath = _clean_up_extract_path(meta.path)
991 add_error(Exception('skipping risky path "%s"' % meta.path))
995 print >> sys.stderr, '+', meta.path
996 _set_up_path(meta, create_symlinks=create_symlinks)
997 if os.path.isdir(meta.path):
998 all_dirs.append(meta)
1001 print >> sys.stderr, '=', meta.path
1002 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1003 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1004 for dir in all_dirs:
1005 # Don't need to check xpath -- won't be in all_dirs if not OK.
1006 xpath = _clean_up_extract_path(dir.path)
1008 print >> sys.stderr, '=', xpath
1009 # Shouldn't have to check for risky paths here (omitted above).
1010 dir.apply_to_path(path=dir.path,
1011 restore_numeric_ids=restore_numeric_ids)