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, format_filesize
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') \
31 or sys.platform.startswith('darwin') \
32 or sys.platform.startswith('netbsd')):
36 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
39 from bup._helpers import get_linux_file_attr, set_linux_file_attr
41 # No need for a warning here; the only reason they won't exist is that we're
42 # not on Linux, in which case files don't have any linux attrs anyway, so
43 # lacking the functions isn't a problem.
44 get_linux_file_attr = set_linux_file_attr = None
47 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
49 # Q: Consider hardlink support?
50 # Q: Is it OK to store raw linux attr (chattr) flags?
51 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
52 # Q: Is the application of posix1e has_extended() correct?
53 # Q: Is one global --numeric-ids argument sufficient?
54 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
55 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
57 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
58 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
59 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
60 # FIXME: Consider pack('vvvvsss', ...) optimization.
61 # FIXME: Consider caching users/groups.
65 # osx (varies between hfs and hfs+):
66 # type - regular dir char block fifo socket ...
67 # perms - rwxrwxrwxsgt
68 # times - ctime atime mtime
71 # hard-link-info (hfs+ only)
74 # attributes-osx see chflags
80 # type - regular dir ...
81 # times - creation, modification, posix change, access
84 # attributes - see attrib
86 # forks (alternate data streams)
90 # type - regular dir ...
91 # perms - rwxrwxrwx (maybe - see wikipedia)
92 # times - creation, modification, access
93 # attributes - see attrib
97 _have_lchmod = hasattr(os, 'lchmod')
100 def _clean_up_path_for_archive(p):
101 # Not the most efficient approach.
104 # Take everything after any '/../'.
105 pos = result.rfind('/../')
107 result = result[result.rfind('/../') + 4:]
109 # Take everything after any remaining '../'.
110 if result.startswith("../"):
113 # Remove any '/./' sequences.
114 pos = result.find('/./')
116 result = result[0:pos] + '/' + result[pos + 3:]
117 pos = result.find('/./')
119 # Remove any leading '/'s.
120 result = result.lstrip('/')
122 # Replace '//' with '/' everywhere.
123 pos = result.find('//')
125 result = result[0:pos] + '/' + result[pos + 2:]
126 pos = result.find('//')
128 # Take everything after any remaining './'.
129 if result.startswith('./'):
132 # Take everything before any remaining '/.'.
133 if result.endswith('/.'):
136 if result == '' or result.endswith('/..'):
143 if p.startswith('/'):
145 if p.find('/../') != -1:
147 if p.startswith('../'):
149 if p.endswith('/..'):
154 def _clean_up_extract_path(p):
155 result = p.lstrip('/')
158 elif _risky_path(result):
164 # These tags are currently conceptually private to Metadata, and they
165 # must be unique, and must *never* be changed.
168 _rec_tag_common = 2 # times, user, group, type, perms, etc. (legacy/broken)
169 _rec_tag_symlink_target = 3
170 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
171 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
172 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
173 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
174 _rec_tag_hardlink_target = 8 # hard link target path
175 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
178 class ApplyError(Exception):
179 # Thrown when unable to apply any given bit of metadata to a path.
184 # Metadata is stored as a sequence of tagged binary records. Each
185 # record will have some subset of add, encode, load, create, and
186 # apply methods, i.e. _add_foo...
188 # We do allow an "empty" object as a special case, i.e. no
189 # records. One can be created by trying to write Metadata(), and
190 # for such an object, read() will return None. This is used by
191 # "bup save", for example, as a placeholder in cases where
194 # NOTE: if any relevant fields are added or removed, be sure to
195 # update same_file() below.
199 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
200 # must be non-negative and < 10**9.
202 def _add_common(self, path, st):
205 self.atime = st.st_atime
206 self.mtime = st.st_mtime
207 self.ctime = st.st_ctime
208 self.user = self.group = ''
209 entry = pwd_from_uid(st.st_uid)
211 self.user = entry.pw_name
212 entry = grp_from_gid(st.st_gid)
214 self.group = entry.gr_name
215 self.mode = st.st_mode
216 # Only collect st_rdev if we might need it for a mknod()
217 # during restore. On some platforms (i.e. kFreeBSD), it isn't
218 # stable for other file types. For example "cp -a" will
219 # change it for a plain file.
220 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
221 self.rdev = st.st_rdev
225 def _same_common(self, other):
226 """Return true or false to indicate similarity in the hardlink sense."""
227 return self.uid == other.uid \
228 and self.gid == other.gid \
229 and self.rdev == other.rdev \
230 and self.mtime == other.mtime \
231 and self.ctime == other.ctime \
232 and self.user == other.user \
233 and self.group == other.group
235 def _encode_common(self):
238 atime = xstat.nsecs_to_timespec(self.atime)
239 mtime = xstat.nsecs_to_timespec(self.mtime)
240 ctime = xstat.nsecs_to_timespec(self.ctime)
241 result = vint.pack('vvsvsvvVvVvV',
256 def _load_common_rec(self, port, legacy_format=False):
257 unpack_fmt = 'vvsvsvvVvVvV'
259 unpack_fmt = 'VVsVsVvVvVvV'
260 data = vint.read_bvec(port)
272 ctime_ns) = vint.unpack(unpack_fmt, data)
273 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
274 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
275 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
277 def _recognized_file_type(self):
278 return stat.S_ISREG(self.mode) \
279 or stat.S_ISDIR(self.mode) \
280 or stat.S_ISCHR(self.mode) \
281 or stat.S_ISBLK(self.mode) \
282 or stat.S_ISFIFO(self.mode) \
283 or stat.S_ISSOCK(self.mode) \
284 or stat.S_ISLNK(self.mode)
286 def _create_via_common_rec(self, path, create_symlinks=True):
288 raise ApplyError('no metadata - cannot create path ' + path)
290 # If the path already exists and is a dir, try rmdir.
291 # If the path already exists and is anything else, try unlink.
294 st = xstat.lstat(path)
296 if e.errno != errno.ENOENT:
299 if stat.S_ISDIR(st.st_mode):
303 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
304 msg = 'refusing to overwrite non-empty dir ' + path
310 if stat.S_ISREG(self.mode):
311 assert(self._recognized_file_type())
312 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
314 elif stat.S_ISDIR(self.mode):
315 assert(self._recognized_file_type())
317 elif stat.S_ISCHR(self.mode):
318 assert(self._recognized_file_type())
319 os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
320 elif stat.S_ISBLK(self.mode):
321 assert(self._recognized_file_type())
322 os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
323 elif stat.S_ISFIFO(self.mode):
324 assert(self._recognized_file_type())
325 os.mknod(path, 0600 | stat.S_IFIFO)
326 elif stat.S_ISSOCK(self.mode):
328 os.mknod(path, 0600 | stat.S_IFSOCK)
330 if e.errno in (errno.EINVAL, errno.EPERM):
331 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
335 elif stat.S_ISLNK(self.mode):
336 assert(self._recognized_file_type())
337 if self.symlink_target and create_symlinks:
338 # on MacOS, symlink() permissions depend on umask, and there's
339 # no way to chown a symlink after creating it, so we have to
341 oldumask = os.umask((self.mode & 0777) ^ 0777)
343 os.symlink(self.symlink_target, path)
346 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
348 assert(not self._recognized_file_type())
349 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
352 def _apply_common_rec(self, path, restore_numeric_ids=False):
354 raise ApplyError('no metadata - cannot apply to ' + path)
356 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
357 # EACCES errors at this stage are fatal for the current path.
358 if lutime and stat.S_ISLNK(self.mode):
360 lutime(path, (self.atime, self.mtime))
362 if e.errno == errno.EACCES:
363 raise ApplyError('lutime: %s' % e)
368 utime(path, (self.atime, self.mtime))
370 if e.errno == errno.EACCES:
371 raise ApplyError('utime: %s' % e)
375 # Implement tar/rsync-like semantics; see bup-restore(1).
376 # FIXME: should we consider caching user/group name <-> id
377 # mappings, getgroups(), etc.?
378 uid = gid = -1 # By default, do nothing.
382 if not restore_numeric_ids:
383 if self.uid != 0 and self.user:
384 entry = pwd_from_name(self.user)
387 if self.gid != 0 and self.group:
388 entry = grp_from_name(self.group)
391 else: # not superuser - only consider changing the group/gid
392 user_gids = os.getgroups()
393 if self.gid in user_gids:
395 if not restore_numeric_ids and self.gid != 0:
396 # The grp might not exist on the local system.
397 grps = filter(None, [grp_from_gid(x) for x in user_gids])
398 if self.group in [x.gr_name for x in grps]:
399 g = grp_from_name(self.group)
403 if uid != -1 or gid != -1:
405 os.lchown(path, uid, gid)
407 if e.errno == errno.EPERM:
408 add_error('lchown: %s' % e)
409 elif sys.platform.startswith('cygwin') \
410 and e.errno == errno.EINVAL:
411 add_error('lchown: unknown uid/gid (%d/%d) for %s'
417 os.lchmod(path, stat.S_IMODE(self.mode))
418 elif not stat.S_ISLNK(self.mode):
419 os.chmod(path, stat.S_IMODE(self.mode))
424 def _encode_path(self):
426 return vint.pack('s', self.path)
430 def _load_path_rec(self, port):
431 self.path = vint.unpack('s', vint.read_bvec(port))[0]
436 def _add_symlink_target(self, path, st):
438 if stat.S_ISLNK(st.st_mode):
439 self.symlink_target = os.readlink(path)
441 add_error('readlink: %s', e)
443 def _encode_symlink_target(self):
444 return self.symlink_target
446 def _load_symlink_target_rec(self, port):
447 self.symlink_target = vint.read_bvec(port)
452 def _add_hardlink_target(self, target):
453 self.hardlink_target = target
455 def _same_hardlink_target(self, other):
456 """Return true or false to indicate similarity in the hardlink sense."""
457 return self.hardlink_target == other.hardlink_target
459 def _encode_hardlink_target(self):
460 return self.hardlink_target
462 def _load_hardlink_target_rec(self, port):
463 self.hardlink_target = vint.read_bvec(port)
466 ## POSIX1e ACL records
468 # Recorded as a list:
469 # [txt_id_acl, num_id_acl]
470 # or, if a directory:
471 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
472 # The numeric/text distinction only matters when reading/restoring
474 def _add_posix1e_acl(self, path, st):
475 if not posix1e: return
476 if not stat.S_ISLNK(st.st_mode):
480 if posix1e.has_extended(path):
481 acl = posix1e.ACL(file=path)
482 acls = [acl, acl] # txt and num are the same
483 if stat.S_ISDIR(st.st_mode):
484 def_acl = posix1e.ACL(filedef=path)
485 def_acls = [def_acl, def_acl]
486 except EnvironmentError, e:
487 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
490 txt_flags = posix1e.TEXT_ABBREVIATE
491 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
492 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
493 acls[1].to_any_text('', '\n', num_flags)]
495 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
496 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
497 self.posix1e_acl = acl_rep
499 def _same_posix1e_acl(self, other):
500 """Return true or false to indicate similarity in the hardlink sense."""
501 return self.posix1e_acl == other.posix1e_acl
503 def _encode_posix1e_acl(self):
504 # Encode as two strings (w/default ACL string possibly empty).
506 acls = self.posix1e_acl
508 acls.extend(['', ''])
509 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
513 def _load_posix1e_acl_rec(self, port):
514 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
516 acl_rep = acl_rep[:2]
517 self.posix1e_acl = acl_rep
519 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
520 def apply_acl(acl_rep, kind):
522 acl = posix1e.ACL(text = acl_rep)
525 # pylibacl appears to return an IOError with errno
526 # set to 0 if a group referred to by the ACL rep
527 # doesn't exist on the current system.
528 raise ApplyError("POSIX1e ACL: can't create %r for %r"
533 acl.applyto(path, kind)
535 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
536 raise ApplyError('POSIX1e ACL applyto: %s' % e)
542 add_error("%s: can't restore ACLs; posix1e support missing.\n"
546 acls = self.posix1e_acl
548 if restore_numeric_ids:
549 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
551 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
552 if restore_numeric_ids:
553 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
555 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
558 ## Linux attributes (lsattr(1), chattr(1))
560 def _add_linux_attr(self, path, st):
561 if not get_linux_file_attr: return
562 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
564 attr = get_linux_file_attr(path)
566 self.linux_attr = attr
568 if e.errno == errno.EACCES:
569 add_error('read Linux attr: %s' % e)
570 elif e.errno in (errno.ENOTTY, errno.ENOSYS, errno.EOPNOTSUPP):
571 # Assume filesystem doesn't support attrs.
576 def _same_linux_attr(self, other):
577 """Return true or false to indicate similarity in the hardlink sense."""
578 return self.linux_attr == other.linux_attr
580 def _encode_linux_attr(self):
582 return vint.pack('V', self.linux_attr)
586 def _load_linux_attr_rec(self, port):
587 data = vint.read_bvec(port)
588 self.linux_attr = vint.unpack('V', data)[0]
590 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
592 if not set_linux_file_attr:
593 add_error("%s: can't restore linuxattrs: "
594 "linuxattr support missing.\n" % path)
597 set_linux_file_attr(path, self.linux_attr)
599 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS,
601 raise ApplyError('Linux chattr: %s (0x%s)'
602 % (e, hex(self.linux_attr)))
607 ## Linux extended attributes (getfattr(1), setfattr(1))
609 def _add_linux_xattr(self, path, st):
612 self.linux_xattr = xattr.get_all(path, nofollow=True)
613 except EnvironmentError, e:
614 if e.errno != errno.EOPNOTSUPP:
617 def _same_linux_xattr(self, other):
618 """Return true or false to indicate similarity in the hardlink sense."""
619 return self.linux_xattr == other.linux_xattr
621 def _encode_linux_xattr(self):
623 result = vint.pack('V', len(self.linux_xattr))
624 for name, value in self.linux_xattr:
625 result += vint.pack('ss', name, value)
630 def _load_linux_xattr_rec(self, file):
631 data = vint.read_bvec(file)
632 memfile = StringIO(data)
634 for i in range(vint.read_vuint(memfile)):
635 key = vint.read_bvec(memfile)
636 value = vint.read_bvec(memfile)
637 result.append((key, value))
638 self.linux_xattr = result
640 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
643 add_error("%s: can't restore xattr; xattr support missing.\n"
646 if not self.linux_xattr:
649 existing_xattrs = set(xattr.list(path, nofollow=True))
651 if e.errno == errno.EACCES:
652 raise ApplyError('xattr.set: %s' % e)
655 for k, v in self.linux_xattr:
656 if k not in existing_xattrs \
657 or v != xattr.get(path, k, nofollow=True):
659 xattr.set(path, k, v, nofollow=True)
661 if e.errno == errno.EPERM \
662 or e.errno == errno.EOPNOTSUPP:
663 raise ApplyError('xattr.set: %s' % e)
666 existing_xattrs -= frozenset([k])
667 for k in existing_xattrs:
669 xattr.remove(path, k, nofollow=True)
671 if e.errno == errno.EPERM:
672 raise ApplyError('xattr.remove: %s' % e)
681 self.symlink_target = None
682 self.hardlink_target = None
683 self.linux_attr = None
684 self.linux_xattr = None
685 self.posix1e_acl = None
687 def write(self, port, include_path=True):
688 records = include_path and [(_rec_tag_path, self._encode_path())] or []
689 records.extend([(_rec_tag_common_v2, self._encode_common()),
690 (_rec_tag_symlink_target,
691 self._encode_symlink_target()),
692 (_rec_tag_hardlink_target,
693 self._encode_hardlink_target()),
694 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
695 (_rec_tag_linux_attr, self._encode_linux_attr()),
696 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
697 for tag, data in records:
699 vint.write_vuint(port, tag)
700 vint.write_bvec(port, data)
701 vint.write_vuint(port, _rec_tag_end)
703 def encode(self, include_path=True):
705 self.write(port, include_path)
706 return port.getvalue()
710 # This method should either return a valid Metadata object,
711 # return None if there was no information at all (just a
712 # _rec_tag_end), throw EOFError if there was nothing at all to
713 # read, or throw an Exception if a valid object could not be
715 tag = vint.read_vuint(port)
716 if tag == _rec_tag_end:
718 try: # From here on, EOF is an error.
720 while True: # only exit is error (exception) or _rec_tag_end
721 if tag == _rec_tag_path:
722 result._load_path_rec(port)
723 elif tag == _rec_tag_common_v2:
724 result._load_common_rec(port)
725 elif tag == _rec_tag_symlink_target:
726 result._load_symlink_target_rec(port)
727 elif tag == _rec_tag_hardlink_target:
728 result._load_hardlink_target_rec(port)
729 elif tag == _rec_tag_posix1e_acl:
730 result._load_posix1e_acl_rec(port)
731 elif tag == _rec_tag_linux_attr:
732 result._load_linux_attr_rec(port)
733 elif tag == _rec_tag_linux_xattr:
734 result._load_linux_xattr_rec(port)
735 elif tag == _rec_tag_end:
737 elif tag == _rec_tag_common: # Should be very rare.
738 result._load_common_rec(port, legacy_format = True)
739 else: # unknown record
741 tag = vint.read_vuint(port)
743 raise Exception("EOF while reading Metadata")
746 return stat.S_ISDIR(self.mode)
748 def create_path(self, path, create_symlinks=True):
749 self._create_via_common_rec(path, create_symlinks=create_symlinks)
751 def apply_to_path(self, path=None, restore_numeric_ids=False):
752 # apply metadata to path -- file must exist
756 raise Exception('Metadata.apply_to_path() called with no path')
757 if not self._recognized_file_type():
758 add_error('not applying metadata to "%s"' % path
759 + ' with unrecognized mode "0x%x"\n' % self.mode)
761 num_ids = restore_numeric_ids
762 for apply_metadata in (self._apply_common_rec,
763 self._apply_posix1e_acl_rec,
764 self._apply_linux_attr_rec,
765 self._apply_linux_xattr_rec):
767 apply_metadata(path, restore_numeric_ids=num_ids)
768 except ApplyError, e:
771 def same_file(self, other):
772 """Compare this to other for equivalency. Return true if
773 their information implies they could represent the same file
774 on disk, in the hardlink sense. Assume they're both regular
776 return self._same_common(other) \
777 and self._same_hardlink_target(other) \
778 and self._same_posix1e_acl(other) \
779 and self._same_linux_attr(other) \
780 and self._same_linux_xattr(other)
783 def from_path(path, statinfo=None, archive_path=None,
784 save_symlinks=True, hardlink_target=None):
786 result.path = archive_path
787 st = statinfo or xstat.lstat(path)
788 result.size = st.st_size
789 result._add_common(path, st)
791 result._add_symlink_target(path, st)
792 result._add_hardlink_target(hardlink_target)
793 result._add_posix1e_acl(path, st)
794 result._add_linux_attr(path, st)
795 result._add_linux_xattr(path, st)
799 def save_tree(output_file, paths,
805 # Issue top-level rewrite warnings.
807 safe_path = _clean_up_path_for_archive(path)
808 if safe_path != path:
809 log('archiving "%s" as "%s"\n' % (path, safe_path))
813 safe_path = _clean_up_path_for_archive(p)
815 if stat.S_ISDIR(st.st_mode):
817 m = from_path(p, statinfo=st, archive_path=safe_path,
818 save_symlinks=save_symlinks)
820 print >> sys.stderr, m.path
821 m.write(output_file, include_path=write_paths)
823 start_dir = os.getcwd()
825 for (p, st) in recursive_dirlist(paths, xdev=xdev):
826 dirlist_dir = os.getcwd()
828 safe_path = _clean_up_path_for_archive(p)
829 m = from_path(p, statinfo=st, archive_path=safe_path,
830 save_symlinks=save_symlinks)
832 print >> sys.stderr, m.path
833 m.write(output_file, include_path=write_paths)
834 os.chdir(dirlist_dir)
839 def _set_up_path(meta, create_symlinks=True):
840 # Allow directories to exist as a special case -- might have
841 # been created by an earlier longer path.
845 parent = os.path.dirname(meta.path)
848 meta.create_path(meta.path, create_symlinks=create_symlinks)
851 all_fields = frozenset(['path',
868 def summary_str(meta, numeric_ids = False, human_readable = False):
869 mode_val = xstat.mode_str(meta.mode)
871 if numeric_ids or not user_val:
872 user_val = str(meta.uid)
873 group_val = meta.group
874 if numeric_ids or not group_val:
875 group_val = str(meta.gid)
876 size_or_dev_val = '-'
877 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
878 size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
879 elif meta.size != None:
880 size_or_dev_val = meta.size
882 size_or_dev_val = format_filesize(meta.size)
883 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
884 time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
885 path_val = meta.path or ''
886 if stat.S_ISLNK(meta.mode):
887 path_val += ' -> ' + meta.symlink_target
888 return '%-10s %-11s %11s %16s %s' % (mode_val,
889 user_val + "/" + group_val,
895 def detailed_str(meta, fields = None):
896 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
897 # 0", "link-target:", etc.
903 path = meta.path or ''
904 result.append('path: ' + path)
906 result.append('mode: %s (%s)' % (oct(meta.mode),
907 xstat.mode_str(meta.mode)))
908 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
909 result.append('link-target: ' + meta.symlink_target)
912 result.append('rdev: %d,%d' % (os.major(meta.rdev),
913 os.minor(meta.rdev)))
915 result.append('rdev: 0')
916 if 'size' in fields and meta.size:
917 result.append('size: ' + str(meta.size))
919 result.append('uid: ' + str(meta.uid))
921 result.append('gid: ' + str(meta.gid))
923 result.append('user: ' + meta.user)
924 if 'group' in fields:
925 result.append('group: ' + meta.group)
926 if 'atime' in fields:
927 # If we don't have xstat.lutime, that means we have to use
928 # utime(), and utime() has no way to set the mtime/atime of a
929 # symlink. Thus, the mtime/atime of a symlink is meaningless,
930 # so let's not report it. (That way scripts comparing
931 # before/after won't trigger.)
932 if xstat.lutime or not stat.S_ISLNK(meta.mode):
933 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
935 result.append('atime: 0')
936 if 'mtime' in fields:
937 if xstat.lutime or not stat.S_ISLNK(meta.mode):
938 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
940 result.append('mtime: 0')
941 if 'ctime' in fields:
942 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
943 if 'linux-attr' in fields and meta.linux_attr:
944 result.append('linux-attr: ' + hex(meta.linux_attr))
945 if 'linux-xattr' in fields and meta.linux_xattr:
946 for name, value in meta.linux_xattr:
947 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
948 if 'posix1e-acl' in fields and meta.posix1e_acl:
949 acl = meta.posix1e_acl[0]
950 result.append('posix1e-acl: ' + acl + '\n')
951 if stat.S_ISDIR(meta.mode):
952 def_acl = meta.posix1e_acl[2]
953 result.append('posix1e-acl-default: ' + def_acl + '\n')
954 return '\n'.join(result)
957 class _ArchiveIterator:
960 return Metadata.read(self._file)
962 raise StopIteration()
967 def __init__(self, file):
971 def display_archive(file):
974 for meta in _ArchiveIterator(file):
977 print detailed_str(meta)
980 for meta in _ArchiveIterator(file):
981 print summary_str(meta)
983 for meta in _ArchiveIterator(file):
985 print >> sys.stderr, \
986 'bup: no metadata path, but asked to only display path', \
987 '(increase verbosity?)'
992 def start_extract(file, create_symlinks=True):
993 for meta in _ArchiveIterator(file):
994 if not meta: # Hit end record.
997 print >> sys.stderr, meta.path
998 xpath = _clean_up_extract_path(meta.path)
1000 add_error(Exception('skipping risky path "%s"' % meta.path))
1003 _set_up_path(meta, create_symlinks=create_symlinks)
1006 def finish_extract(file, restore_numeric_ids=False):
1008 for meta in _ArchiveIterator(file):
1009 if not meta: # Hit end record.
1011 xpath = _clean_up_extract_path(meta.path)
1013 add_error(Exception('skipping risky path "%s"' % dir.path))
1015 if os.path.isdir(meta.path):
1016 all_dirs.append(meta)
1019 print >> sys.stderr, meta.path
1020 meta.apply_to_path(path=xpath,
1021 restore_numeric_ids=restore_numeric_ids)
1022 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1023 for dir in all_dirs:
1024 # Don't need to check xpath -- won't be in all_dirs if not OK.
1025 xpath = _clean_up_extract_path(dir.path)
1027 print >> sys.stderr, dir.path
1028 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1031 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1032 # For now, just store all the directories and handle them last,
1035 for meta in _ArchiveIterator(file):
1036 if not meta: # Hit end record.
1038 xpath = _clean_up_extract_path(meta.path)
1040 add_error(Exception('skipping risky path "%s"' % meta.path))
1044 print >> sys.stderr, '+', meta.path
1045 _set_up_path(meta, create_symlinks=create_symlinks)
1046 if os.path.isdir(meta.path):
1047 all_dirs.append(meta)
1050 print >> sys.stderr, '=', meta.path
1051 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1052 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1053 for dir in all_dirs:
1054 # Don't need to check xpath -- won't be in all_dirs if not OK.
1055 xpath = _clean_up_extract_path(dir.path)
1057 print >> sys.stderr, '=', xpath
1058 # Shouldn't have to check for risky paths here (omitted above).
1059 dir.apply_to_path(path=dir.path,
1060 restore_numeric_ids=restore_numeric_ids)