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? (unimplemented)
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.atime = st.st_atime
203 self.mtime = st.st_mtime
204 self.ctime = st.st_ctime
205 self.user = self.group = ''
206 entry = pwd_from_uid(st.st_uid)
208 self.user = entry.pw_name
209 entry = grp_from_gid(st.st_gid)
211 self.group = entry.gr_name
212 self.mode = st.st_mode
213 # Only collect st_rdev if we might need it for a mknod()
214 # during restore. On some platforms (i.e. kFreeBSD), it isn't
215 # stable for other file types. For example "cp -a" will
216 # change it for a plain file.
217 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
218 self.rdev = st.st_rdev
222 def _same_common(self, other):
223 """Return true or false to indicate similarity in the hardlink sense."""
224 return self.uid == other.uid \
225 and self.gid == other.gid \
226 and self.rdev == other.rdev \
227 and self.atime == other.atime \
228 and self.mtime == other.mtime \
229 and self.ctime == other.ctime \
230 and self.user == other.user \
231 and self.group == other.group
233 def _encode_common(self):
236 atime = xstat.nsecs_to_timespec(self.atime)
237 mtime = xstat.nsecs_to_timespec(self.mtime)
238 ctime = xstat.nsecs_to_timespec(self.ctime)
239 result = vint.pack('VVsVsVvVvVvV',
254 def _load_common_rec(self, port):
255 data = vint.read_bvec(port)
267 ctime_ns) = vint.unpack('VVsVsVvVvVvV', data)
268 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
269 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
270 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
272 def _recognized_file_type(self):
273 return stat.S_ISREG(self.mode) \
274 or stat.S_ISDIR(self.mode) \
275 or stat.S_ISCHR(self.mode) \
276 or stat.S_ISBLK(self.mode) \
277 or stat.S_ISFIFO(self.mode) \
278 or stat.S_ISSOCK(self.mode) \
279 or stat.S_ISLNK(self.mode)
281 def _create_via_common_rec(self, path, create_symlinks=True):
283 raise ApplyError('no metadata - cannot create path ' + path)
285 # If the path already exists and is a dir, try rmdir.
286 # If the path already exists and is anything else, try unlink.
289 st = xstat.lstat(path)
291 if e.errno != errno.ENOENT:
294 if stat.S_ISDIR(st.st_mode):
298 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
299 msg = 'refusing to overwrite non-empty dir ' + path
305 if stat.S_ISREG(self.mode):
306 assert(self._recognized_file_type())
307 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
309 elif stat.S_ISDIR(self.mode):
310 assert(self._recognized_file_type())
312 elif stat.S_ISCHR(self.mode):
313 assert(self._recognized_file_type())
314 os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
315 elif stat.S_ISBLK(self.mode):
316 assert(self._recognized_file_type())
317 os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
318 elif stat.S_ISFIFO(self.mode):
319 assert(self._recognized_file_type())
320 os.mknod(path, 0600 | stat.S_IFIFO)
321 elif stat.S_ISSOCK(self.mode):
323 os.mknod(path, 0600 | stat.S_IFSOCK)
325 if e.errno in (errno.EINVAL, errno.EPERM):
326 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
330 elif stat.S_ISLNK(self.mode):
331 assert(self._recognized_file_type())
332 if self.symlink_target and create_symlinks:
333 # on MacOS, symlink() permissions depend on umask, and there's
334 # no way to chown a symlink after creating it, so we have to
336 oldumask = os.umask((self.mode & 0777) ^ 0777)
338 os.symlink(self.symlink_target, path)
341 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
343 assert(not self._recognized_file_type())
344 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
347 def _apply_common_rec(self, path, restore_numeric_ids=False):
349 raise ApplyError('no metadata - cannot apply to ' + path)
351 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
352 # EACCES errors at this stage are fatal for the current path.
353 if lutime and stat.S_ISLNK(self.mode):
355 lutime(path, (self.atime, self.mtime))
357 if e.errno == errno.EACCES:
358 raise ApplyError('lutime: %s' % e)
363 utime(path, (self.atime, self.mtime))
365 if e.errno == errno.EACCES:
366 raise ApplyError('utime: %s' % e)
370 # Implement tar/rsync-like semantics; see bup-restore(1).
371 # FIXME: should we consider caching user/group name <-> id
372 # mappings, getgroups(), etc.?
373 uid = gid = -1 # By default, do nothing.
377 if not restore_numeric_ids:
378 if self.uid != 0 and self.user:
379 entry = pwd_from_name(self.user)
382 if self.gid != 0 and self.group:
383 entry = grp_from_name(self.group)
386 else: # not superuser - only consider changing the group/gid
387 user_gids = os.getgroups()
388 if self.gid in user_gids:
390 if not restore_numeric_ids and self.gid != 0:
391 # The grp might not exist on the local system.
392 grps = filter(None, [grp_from_gid(x) for x in user_gids])
393 if self.group in [x.gr_name for x in grps]:
394 g = grp_from_name(self.group)
398 if uid != -1 or gid != -1:
400 os.lchown(path, uid, gid)
402 if e.errno == errno.EPERM:
403 add_error('lchown: %s' % e)
404 elif sys.platform.startswith('cygwin') \
405 and e.errno == errno.EINVAL:
406 add_error('lchown: unknown uid/gid (%d/%d) for %s'
412 os.lchmod(path, stat.S_IMODE(self.mode))
413 elif not stat.S_ISLNK(self.mode):
414 os.chmod(path, stat.S_IMODE(self.mode))
419 def _encode_path(self):
421 return vint.pack('s', self.path)
425 def _load_path_rec(self, port):
426 self.path = vint.unpack('s', vint.read_bvec(port))[0]
431 def _add_symlink_target(self, path, st):
433 if stat.S_ISLNK(st.st_mode):
434 self.symlink_target = os.readlink(path)
436 add_error('readlink: %s', e)
438 def _encode_symlink_target(self):
439 return self.symlink_target
441 def _load_symlink_target_rec(self, port):
442 self.symlink_target = vint.read_bvec(port)
447 def _add_hardlink_target(self, target):
448 self.hardlink_target = target
450 def _same_hardlink_target(self, other):
451 """Return true or false to indicate similarity in the hardlink sense."""
452 return self.hardlink_target == other.hardlink_target
454 def _encode_hardlink_target(self):
455 return self.hardlink_target
457 def _load_hardlink_target_rec(self, port):
458 self.hardlink_target = vint.read_bvec(port)
461 ## POSIX1e ACL records
463 # Recorded as a list:
464 # [txt_id_acl, num_id_acl]
465 # or, if a directory:
466 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
467 # The numeric/text distinction only matters when reading/restoring
469 def _add_posix1e_acl(self, path, st):
470 if not posix1e: return
471 if not stat.S_ISLNK(st.st_mode):
475 if posix1e.has_extended(path):
476 acl = posix1e.ACL(file=path)
477 acls = [acl, acl] # txt and num are the same
478 if stat.S_ISDIR(st.st_mode):
479 def_acl = posix1e.ACL(filedef=path)
480 def_acls = [def_acl, def_acl]
481 except EnvironmentError, e:
482 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
485 txt_flags = posix1e.TEXT_ABBREVIATE
486 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
487 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
488 acls[1].to_any_text('', '\n', num_flags)]
490 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
491 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
492 self.posix1e_acl = acl_rep
494 def _same_posix1e_acl(self, other):
495 """Return true or false to indicate similarity in the hardlink sense."""
496 return self.posix1e_acl == other.posix1e_acl
498 def _encode_posix1e_acl(self):
499 # Encode as two strings (w/default ACL string possibly empty).
501 acls = self.posix1e_acl
503 acls.extend(['', ''])
504 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
508 def _load_posix1e_acl_rec(self, port):
509 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
511 acl_rep = acl_rep[:2]
512 self.posix1e_acl = acl_rep
514 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
515 def apply_acl(acl_rep, kind):
517 acl = posix1e.ACL(text = acl_rep)
520 # pylibacl appears to return an IOError with errno
521 # set to 0 if a group referred to by the ACL rep
522 # doesn't exist on the current system.
523 raise ApplyError("POSIX1e ACL: can't create %r for %r"
528 acl.applyto(path, kind)
530 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
531 raise ApplyError('POSIX1e ACL applyto: %s' % e)
537 add_error("%s: can't restore ACLs; posix1e support missing.\n"
541 acls = self.posix1e_acl
543 if restore_numeric_ids:
544 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
546 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
547 if restore_numeric_ids:
548 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
550 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
553 ## Linux attributes (lsattr(1), chattr(1))
555 def _add_linux_attr(self, path, st):
556 if not get_linux_file_attr: return
557 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
559 attr = get_linux_file_attr(path)
561 self.linux_attr = attr
563 if e.errno == errno.EACCES:
564 add_error('read Linux attr: %s' % e)
565 elif e.errno in (errno.ENOTTY, errno.ENOSYS, errno.EOPNOTSUPP):
566 # Assume filesystem doesn't support attrs.
571 def _same_linux_attr(self, other):
572 """Return true or false to indicate similarity in the hardlink sense."""
573 return self.linux_attr == other.linux_attr
575 def _encode_linux_attr(self):
577 return vint.pack('V', self.linux_attr)
581 def _load_linux_attr_rec(self, port):
582 data = vint.read_bvec(port)
583 self.linux_attr = vint.unpack('V', data)[0]
585 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
587 if not set_linux_file_attr:
588 add_error("%s: can't restore linuxattrs: "
589 "linuxattr support missing.\n" % path)
592 set_linux_file_attr(path, self.linux_attr)
594 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS,
596 raise ApplyError('Linux chattr: %s' % e)
601 ## Linux extended attributes (getfattr(1), setfattr(1))
603 def _add_linux_xattr(self, path, st):
606 self.linux_xattr = xattr.get_all(path, nofollow=True)
607 except EnvironmentError, e:
608 if e.errno != errno.EOPNOTSUPP:
611 def _same_linux_xattr(self, other):
612 """Return true or false to indicate similarity in the hardlink sense."""
613 return self.linux_xattr == other.linux_xattr
615 def _encode_linux_xattr(self):
617 result = vint.pack('V', len(self.linux_xattr))
618 for name, value in self.linux_xattr:
619 result += vint.pack('ss', name, value)
624 def _load_linux_xattr_rec(self, file):
625 data = vint.read_bvec(file)
626 memfile = StringIO(data)
628 for i in range(vint.read_vuint(memfile)):
629 key = vint.read_bvec(memfile)
630 value = vint.read_bvec(memfile)
631 result.append((key, value))
632 self.linux_xattr = result
634 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
637 add_error("%s: can't restore xattr; xattr support missing.\n"
640 if not self.linux_xattr:
643 existing_xattrs = set(xattr.list(path, nofollow=True))
645 if e.errno == errno.EACCES:
646 raise ApplyError('xattr.set: %s' % e)
649 for k, v in self.linux_xattr:
650 if k not in existing_xattrs \
651 or v != xattr.get(path, k, nofollow=True):
653 xattr.set(path, k, v, nofollow=True)
655 if e.errno == errno.EPERM \
656 or e.errno == errno.EOPNOTSUPP:
657 raise ApplyError('xattr.set: %s' % e)
660 existing_xattrs -= frozenset([k])
661 for k in existing_xattrs:
663 xattr.remove(path, k, nofollow=True)
665 if e.errno == errno.EPERM:
666 raise ApplyError('xattr.remove: %s' % e)
675 self.symlink_target = None
676 self.hardlink_target = None
677 self.linux_attr = None
678 self.linux_xattr = None
679 self.posix1e_acl = None
681 def write(self, port, include_path=True):
682 records = include_path and [(_rec_tag_path, self._encode_path())] or []
683 records.extend([(_rec_tag_common, self._encode_common()),
684 (_rec_tag_symlink_target,
685 self._encode_symlink_target()),
686 (_rec_tag_hardlink_target,
687 self._encode_hardlink_target()),
688 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
689 (_rec_tag_linux_attr, self._encode_linux_attr()),
690 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
691 for tag, data in records:
693 vint.write_vuint(port, tag)
694 vint.write_bvec(port, data)
695 vint.write_vuint(port, _rec_tag_end)
697 def encode(self, include_path=True):
699 self.write(port, include_path)
700 return port.getvalue()
704 # This method should either return a valid Metadata object,
705 # return None if there was no information at all (just a
706 # _rec_tag_end), throw EOFError if there was nothing at all to
707 # read, or throw an Exception if a valid object could not be
709 tag = vint.read_vuint(port)
710 if tag == _rec_tag_end:
712 try: # From here on, EOF is an error.
714 while True: # only exit is error (exception) or _rec_tag_end
715 if tag == _rec_tag_path:
716 result._load_path_rec(port)
717 elif tag == _rec_tag_common:
718 result._load_common_rec(port)
719 elif tag == _rec_tag_symlink_target:
720 result._load_symlink_target_rec(port)
721 elif tag == _rec_tag_hardlink_target:
722 result._load_hardlink_target_rec(port)
723 elif tag == _rec_tag_posix1e_acl:
724 result._load_posix1e_acl_rec(port)
725 elif tag == _rec_tag_linux_attr:
726 result._load_linux_attr_rec(port)
727 elif tag == _rec_tag_linux_xattr:
728 result._load_linux_xattr_rec(port)
729 elif tag == _rec_tag_end:
731 else: # unknown record
733 tag = vint.read_vuint(port)
735 raise Exception("EOF while reading Metadata")
738 return stat.S_ISDIR(self.mode)
740 def create_path(self, path, create_symlinks=True):
741 self._create_via_common_rec(path, create_symlinks=create_symlinks)
743 def apply_to_path(self, path=None, restore_numeric_ids=False):
744 # apply metadata to path -- file must exist
748 raise Exception('Metadata.apply_to_path() called with no path')
749 if not self._recognized_file_type():
750 add_error('not applying metadata to "%s"' % path
751 + ' with unrecognized mode "0x%x"\n' % self.mode)
753 num_ids = restore_numeric_ids
754 for apply_metadata in (self._apply_common_rec,
755 self._apply_posix1e_acl_rec,
756 self._apply_linux_attr_rec,
757 self._apply_linux_xattr_rec):
759 apply_metadata(path, restore_numeric_ids=num_ids)
760 except ApplyError, e:
763 def same_file(self, other):
764 """Compare this to other for equivalency. Return true if
765 their information implies they could represent the same file
766 on disk, in the hardlink sense. Assume they're both regular
768 return self._same_common(other) \
769 and self._same_hardlink_target(other) \
770 and self._same_posix1e_acl(other) \
771 and self._same_linux_attr(other) \
772 and self._same_linux_xattr(other)
775 def from_path(path, statinfo=None, archive_path=None,
776 save_symlinks=True, hardlink_target=None):
778 result.path = archive_path
779 st = statinfo or xstat.lstat(path)
780 result.size = st.st_size
781 result._add_common(path, st)
783 result._add_symlink_target(path, st)
784 result._add_hardlink_target(hardlink_target)
785 result._add_posix1e_acl(path, st)
786 result._add_linux_attr(path, st)
787 result._add_linux_xattr(path, st)
791 def save_tree(output_file, paths,
797 # Issue top-level rewrite warnings.
799 safe_path = _clean_up_path_for_archive(path)
800 if safe_path != path:
801 log('archiving "%s" as "%s"\n' % (path, safe_path))
805 safe_path = _clean_up_path_for_archive(p)
807 if stat.S_ISDIR(st.st_mode):
809 m = from_path(p, statinfo=st, archive_path=safe_path,
810 save_symlinks=save_symlinks)
812 print >> sys.stderr, m.path
813 m.write(output_file, include_path=write_paths)
815 start_dir = os.getcwd()
817 for (p, st) in recursive_dirlist(paths, xdev=xdev):
818 dirlist_dir = os.getcwd()
820 safe_path = _clean_up_path_for_archive(p)
821 m = from_path(p, statinfo=st, archive_path=safe_path,
822 save_symlinks=save_symlinks)
824 print >> sys.stderr, m.path
825 m.write(output_file, include_path=write_paths)
826 os.chdir(dirlist_dir)
831 def _set_up_path(meta, create_symlinks=True):
832 # Allow directories to exist as a special case -- might have
833 # been created by an earlier longer path.
837 parent = os.path.dirname(meta.path)
840 meta.create_path(meta.path, create_symlinks=create_symlinks)
843 all_fields = frozenset(['path',
860 def summary_str(meta):
861 mode_val = xstat.mode_str(meta.mode)
864 user_val = str(meta.uid)
865 group_val = meta.group
867 group_val = str(meta.gid)
868 size_or_dev_val = '-'
869 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
870 size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
872 size_or_dev_val = meta.size
873 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
874 time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
875 path_val = meta.path or ''
876 if stat.S_ISLNK(meta.mode):
877 path_val += ' -> ' + meta.symlink_target
878 return '%-10s %-11s %11s %16s %s' % (mode_val,
879 user_val + "/" + group_val,
885 def detailed_str(meta, fields = None):
886 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
887 # 0", "link-target:", etc.
893 path = meta.path or ''
894 result.append('path: ' + path)
896 result.append('mode: %s (%s)' % (oct(meta.mode),
897 xstat.mode_str(meta.mode)))
898 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
899 result.append('link-target: ' + meta.symlink_target)
902 result.append('rdev: %d,%d' % (os.major(meta.rdev),
903 os.minor(meta.rdev)))
905 result.append('rdev: 0')
906 if 'size' in fields and meta.size:
907 result.append('size: ' + str(meta.size))
909 result.append('uid: ' + str(meta.uid))
911 result.append('gid: ' + str(meta.gid))
913 result.append('user: ' + meta.user)
914 if 'group' in fields:
915 result.append('group: ' + meta.group)
916 if 'atime' in fields:
917 # If we don't have xstat.lutime, that means we have to use
918 # utime(), and utime() has no way to set the mtime/atime of a
919 # symlink. Thus, the mtime/atime of a symlink is meaningless,
920 # so let's not report it. (That way scripts comparing
921 # before/after won't trigger.)
922 if xstat.lutime or not stat.S_ISLNK(meta.mode):
923 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
925 result.append('atime: 0')
926 if 'mtime' in fields:
927 if xstat.lutime or not stat.S_ISLNK(meta.mode):
928 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
930 result.append('mtime: 0')
931 if 'ctime' in fields:
932 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
933 if 'linux-attr' in fields and meta.linux_attr:
934 result.append('linux-attr: ' + hex(meta.linux_attr))
935 if 'linux-xattr' in fields and meta.linux_xattr:
936 for name, value in meta.linux_xattr:
937 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
938 if 'posix1e-acl' in fields and meta.posix1e_acl:
939 acl = meta.posix1e_acl[0]
940 result.append('posix1e-acl: ' + acl + '\n')
941 if stat.S_ISDIR(meta.mode):
942 def_acl = meta.posix1e_acl[2]
943 result.append('posix1e-acl-default: ' + def_acl + '\n')
944 return '\n'.join(result)
947 class _ArchiveIterator:
950 return Metadata.read(self._file)
952 raise StopIteration()
957 def __init__(self, file):
961 def display_archive(file):
964 for meta in _ArchiveIterator(file):
967 print detailed_str(meta)
970 for meta in _ArchiveIterator(file):
971 print summary_str(meta)
973 for meta in _ArchiveIterator(file):
975 print >> sys.stderr, \
976 'bup: no metadata path, but asked to only display path', \
977 '(increase verbosity?)'
982 def start_extract(file, create_symlinks=True):
983 for meta in _ArchiveIterator(file):
984 if not meta: # Hit end record.
987 print >> sys.stderr, meta.path
988 xpath = _clean_up_extract_path(meta.path)
990 add_error(Exception('skipping risky path "%s"' % meta.path))
993 _set_up_path(meta, create_symlinks=create_symlinks)
996 def finish_extract(file, restore_numeric_ids=False):
998 for meta in _ArchiveIterator(file):
999 if not meta: # Hit end record.
1001 xpath = _clean_up_extract_path(meta.path)
1003 add_error(Exception('skipping risky path "%s"' % dir.path))
1005 if os.path.isdir(meta.path):
1006 all_dirs.append(meta)
1009 print >> sys.stderr, meta.path
1010 meta.apply_to_path(path=xpath,
1011 restore_numeric_ids=restore_numeric_ids)
1012 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1013 for dir in all_dirs:
1014 # Don't need to check xpath -- won't be in all_dirs if not OK.
1015 xpath = _clean_up_extract_path(dir.path)
1017 print >> sys.stderr, dir.path
1018 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1021 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1022 # For now, just store all the directories and handle them last,
1025 for meta in _ArchiveIterator(file):
1026 if not meta: # Hit end record.
1028 xpath = _clean_up_extract_path(meta.path)
1030 add_error(Exception('skipping risky path "%s"' % meta.path))
1034 print >> sys.stderr, '+', meta.path
1035 _set_up_path(meta, create_symlinks=create_symlinks)
1036 if os.path.isdir(meta.path):
1037 all_dirs.append(meta)
1040 print >> sys.stderr, '=', meta.path
1041 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1042 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1043 for dir in all_dirs:
1044 # Don't need to check xpath -- won't be in all_dirs if not OK.
1045 xpath = _clean_up_extract_path(dir.path)
1047 print >> sys.stderr, '=', xpath
1048 # Shouldn't have to check for risky paths here (omitted above).
1049 dir.apply_to_path(path=dir.path,
1050 restore_numeric_ids=restore_numeric_ids)