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.
64 # osx (varies between hfs and hfs+):
65 # type - regular dir char block fifo socket ...
66 # perms - rwxrwxrwxsgt
67 # times - ctime atime mtime
70 # hard-link-info (hfs+ only)
73 # attributes-osx see chflags
79 # type - regular dir ...
80 # times - creation, modification, posix change, access
83 # attributes - see attrib
85 # forks (alternate data streams)
89 # type - regular dir ...
90 # perms - rwxrwxrwx (maybe - see wikipedia)
91 # times - creation, modification, access
92 # attributes - see attrib
96 _have_lchmod = hasattr(os, 'lchmod')
99 def _clean_up_path_for_archive(p):
100 # Not the most efficient approach.
103 # Take everything after any '/../'.
104 pos = result.rfind('/../')
106 result = result[result.rfind('/../') + 4:]
108 # Take everything after any remaining '../'.
109 if result.startswith("../"):
112 # Remove any '/./' sequences.
113 pos = result.find('/./')
115 result = result[0:pos] + '/' + result[pos + 3:]
116 pos = result.find('/./')
118 # Remove any leading '/'s.
119 result = result.lstrip('/')
121 # Replace '//' with '/' everywhere.
122 pos = result.find('//')
124 result = result[0:pos] + '/' + result[pos + 2:]
125 pos = result.find('//')
127 # Take everything after any remaining './'.
128 if result.startswith('./'):
131 # Take everything before any remaining '/.'.
132 if result.endswith('/.'):
135 if result == '' or result.endswith('/..'):
142 if p.startswith('/'):
144 if p.find('/../') != -1:
146 if p.startswith('../'):
148 if p.endswith('/..'):
153 def _clean_up_extract_path(p):
154 result = p.lstrip('/')
157 elif _risky_path(result):
163 # These tags are currently conceptually private to Metadata, and they
164 # must be unique, and must *never* be changed.
167 _rec_tag_common = 2 # times, user, group, type, perms, etc. (legacy/broken)
168 _rec_tag_symlink_target = 3
169 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
170 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
171 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
172 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
173 _rec_tag_hardlink_target = 8 # hard link target path
174 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
177 class ApplyError(Exception):
178 # Thrown when unable to apply any given bit of metadata to a path.
183 # Metadata is stored as a sequence of tagged binary records. Each
184 # record will have some subset of add, encode, load, create, and
185 # apply methods, i.e. _add_foo...
187 # We do allow an "empty" object as a special case, i.e. no
188 # records. One can be created by trying to write Metadata(), and
189 # for such an object, read() will return None. This is used by
190 # "bup save", for example, as a placeholder in cases where
193 # NOTE: if any relevant fields are added or removed, be sure to
194 # update same_file() below.
198 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
199 # must be non-negative and < 10**9.
201 def _add_common(self, path, st):
204 self.atime = st.st_atime
205 self.mtime = st.st_mtime
206 self.ctime = st.st_ctime
207 self.user = self.group = ''
208 entry = pwd_from_uid(st.st_uid)
210 self.user = entry.pw_name
211 entry = grp_from_gid(st.st_gid)
213 self.group = entry.gr_name
214 self.mode = st.st_mode
215 # Only collect st_rdev if we might need it for a mknod()
216 # during restore. On some platforms (i.e. kFreeBSD), it isn't
217 # stable for other file types. For example "cp -a" will
218 # change it for a plain file.
219 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
220 self.rdev = st.st_rdev
224 def _same_common(self, other):
225 """Return true or false to indicate similarity in the hardlink sense."""
226 return self.uid == other.uid \
227 and self.gid == other.gid \
228 and self.rdev == other.rdev \
229 and self.mtime == other.mtime \
230 and self.ctime == other.ctime \
231 and self.user == other.user \
232 and self.group == other.group
234 def _encode_common(self):
237 atime = xstat.nsecs_to_timespec(self.atime)
238 mtime = xstat.nsecs_to_timespec(self.mtime)
239 ctime = xstat.nsecs_to_timespec(self.ctime)
240 result = vint.pack('vvsvsvvVvVvV',
255 def _load_common_rec(self, port, legacy_format=False):
256 unpack_fmt = 'vvsvsvvVvVvV'
258 unpack_fmt = 'VVsVsVvVvVvV'
259 data = vint.read_bvec(port)
271 ctime_ns) = vint.unpack(unpack_fmt, data)
272 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
273 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
274 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
276 def _recognized_file_type(self):
277 return stat.S_ISREG(self.mode) \
278 or stat.S_ISDIR(self.mode) \
279 or stat.S_ISCHR(self.mode) \
280 or stat.S_ISBLK(self.mode) \
281 or stat.S_ISFIFO(self.mode) \
282 or stat.S_ISSOCK(self.mode) \
283 or stat.S_ISLNK(self.mode)
285 def _create_via_common_rec(self, path, create_symlinks=True):
287 raise ApplyError('no metadata - cannot create path ' + path)
289 # If the path already exists and is a dir, try rmdir.
290 # If the path already exists and is anything else, try unlink.
293 st = xstat.lstat(path)
295 if e.errno != errno.ENOENT:
298 if stat.S_ISDIR(st.st_mode):
302 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
303 msg = 'refusing to overwrite non-empty dir ' + path
309 if stat.S_ISREG(self.mode):
310 assert(self._recognized_file_type())
311 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
313 elif stat.S_ISDIR(self.mode):
314 assert(self._recognized_file_type())
316 elif stat.S_ISCHR(self.mode):
317 assert(self._recognized_file_type())
318 os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
319 elif stat.S_ISBLK(self.mode):
320 assert(self._recognized_file_type())
321 os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
322 elif stat.S_ISFIFO(self.mode):
323 assert(self._recognized_file_type())
324 os.mknod(path, 0600 | stat.S_IFIFO)
325 elif stat.S_ISSOCK(self.mode):
327 os.mknod(path, 0600 | stat.S_IFSOCK)
329 if e.errno in (errno.EINVAL, errno.EPERM):
330 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
334 elif stat.S_ISLNK(self.mode):
335 assert(self._recognized_file_type())
336 if self.symlink_target and create_symlinks:
337 # on MacOS, symlink() permissions depend on umask, and there's
338 # no way to chown a symlink after creating it, so we have to
340 oldumask = os.umask((self.mode & 0777) ^ 0777)
342 os.symlink(self.symlink_target, path)
345 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
347 assert(not self._recognized_file_type())
348 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
351 def _apply_common_rec(self, path, restore_numeric_ids=False):
353 raise ApplyError('no metadata - cannot apply to ' + path)
355 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
356 # EACCES errors at this stage are fatal for the current path.
357 if lutime and stat.S_ISLNK(self.mode):
359 lutime(path, (self.atime, self.mtime))
361 if e.errno == errno.EACCES:
362 raise ApplyError('lutime: %s' % e)
367 utime(path, (self.atime, self.mtime))
369 if e.errno == errno.EACCES:
370 raise ApplyError('utime: %s' % e)
374 uid = gid = -1 # By default, do nothing.
378 if not restore_numeric_ids:
379 if self.uid != 0 and self.user:
380 entry = pwd_from_name(self.user)
383 if self.gid != 0 and self.group:
384 entry = grp_from_name(self.group)
387 else: # not superuser - only consider changing the group/gid
388 user_gids = os.getgroups()
389 if self.gid in user_gids:
391 if not restore_numeric_ids and self.gid != 0:
392 # The grp might not exist on the local system.
393 grps = filter(None, [grp_from_gid(x) for x in user_gids])
394 if self.group in [x.gr_name for x in grps]:
395 g = grp_from_name(self.group)
399 if uid != -1 or gid != -1:
401 os.lchown(path, uid, gid)
403 if e.errno == errno.EPERM:
404 add_error('lchown: %s' % e)
405 elif sys.platform.startswith('cygwin') \
406 and e.errno == errno.EINVAL:
407 add_error('lchown: unknown uid/gid (%d/%d) for %s'
413 os.lchmod(path, stat.S_IMODE(self.mode))
414 elif not stat.S_ISLNK(self.mode):
415 os.chmod(path, stat.S_IMODE(self.mode))
420 def _encode_path(self):
422 return vint.pack('s', self.path)
426 def _load_path_rec(self, port):
427 self.path = vint.unpack('s', vint.read_bvec(port))[0]
432 def _add_symlink_target(self, path, st):
434 if stat.S_ISLNK(st.st_mode):
435 self.symlink_target = os.readlink(path)
437 add_error('readlink: %s', e)
439 def _encode_symlink_target(self):
440 return self.symlink_target
442 def _load_symlink_target_rec(self, port):
443 self.symlink_target = vint.read_bvec(port)
448 def _add_hardlink_target(self, target):
449 self.hardlink_target = target
451 def _same_hardlink_target(self, other):
452 """Return true or false to indicate similarity in the hardlink sense."""
453 return self.hardlink_target == other.hardlink_target
455 def _encode_hardlink_target(self):
456 return self.hardlink_target
458 def _load_hardlink_target_rec(self, port):
459 self.hardlink_target = vint.read_bvec(port)
462 ## POSIX1e ACL records
464 # Recorded as a list:
465 # [txt_id_acl, num_id_acl]
466 # or, if a directory:
467 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
468 # The numeric/text distinction only matters when reading/restoring
470 def _add_posix1e_acl(self, path, st):
471 if not posix1e: return
472 if not stat.S_ISLNK(st.st_mode):
476 if posix1e.has_extended(path):
477 acl = posix1e.ACL(file=path)
478 acls = [acl, acl] # txt and num are the same
479 if stat.S_ISDIR(st.st_mode):
480 def_acl = posix1e.ACL(filedef=path)
481 def_acls = [def_acl, def_acl]
482 except EnvironmentError, e:
483 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
486 txt_flags = posix1e.TEXT_ABBREVIATE
487 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
488 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
489 acls[1].to_any_text('', '\n', num_flags)]
491 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
492 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
493 self.posix1e_acl = acl_rep
495 def _same_posix1e_acl(self, other):
496 """Return true or false to indicate similarity in the hardlink sense."""
497 return self.posix1e_acl == other.posix1e_acl
499 def _encode_posix1e_acl(self):
500 # Encode as two strings (w/default ACL string possibly empty).
502 acls = self.posix1e_acl
504 acls.extend(['', ''])
505 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
509 def _load_posix1e_acl_rec(self, port):
510 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
512 acl_rep = acl_rep[:2]
513 self.posix1e_acl = acl_rep
515 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
516 def apply_acl(acl_rep, kind):
518 acl = posix1e.ACL(text = acl_rep)
521 # pylibacl appears to return an IOError with errno
522 # set to 0 if a group referred to by the ACL rep
523 # doesn't exist on the current system.
524 raise ApplyError("POSIX1e ACL: can't create %r for %r"
529 acl.applyto(path, kind)
531 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
532 raise ApplyError('POSIX1e ACL applyto: %s' % e)
538 add_error("%s: can't restore ACLs; posix1e support missing.\n"
542 acls = self.posix1e_acl
544 if restore_numeric_ids:
545 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
547 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
548 if restore_numeric_ids:
549 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
551 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
554 ## Linux attributes (lsattr(1), chattr(1))
556 def _add_linux_attr(self, path, st):
557 if not get_linux_file_attr: return
558 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
560 attr = get_linux_file_attr(path)
562 self.linux_attr = attr
564 if e.errno == errno.EACCES:
565 add_error('read Linux attr: %s' % e)
566 elif e.errno in (errno.ENOTTY, errno.ENOSYS, errno.EOPNOTSUPP):
567 # Assume filesystem doesn't support attrs.
572 def _same_linux_attr(self, other):
573 """Return true or false to indicate similarity in the hardlink sense."""
574 return self.linux_attr == other.linux_attr
576 def _encode_linux_attr(self):
578 return vint.pack('V', self.linux_attr)
582 def _load_linux_attr_rec(self, port):
583 data = vint.read_bvec(port)
584 self.linux_attr = vint.unpack('V', data)[0]
586 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
588 if not set_linux_file_attr:
589 add_error("%s: can't restore linuxattrs: "
590 "linuxattr support missing.\n" % path)
593 set_linux_file_attr(path, self.linux_attr)
595 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS,
597 raise ApplyError('Linux chattr: %s (0x%s)'
598 % (e, hex(self.linux_attr)))
603 ## Linux extended attributes (getfattr(1), setfattr(1))
605 def _add_linux_xattr(self, path, st):
608 self.linux_xattr = xattr.get_all(path, nofollow=True)
609 except EnvironmentError, e:
610 if e.errno != errno.EOPNOTSUPP:
613 def _same_linux_xattr(self, other):
614 """Return true or false to indicate similarity in the hardlink sense."""
615 return self.linux_xattr == other.linux_xattr
617 def _encode_linux_xattr(self):
619 result = vint.pack('V', len(self.linux_xattr))
620 for name, value in self.linux_xattr:
621 result += vint.pack('ss', name, value)
626 def _load_linux_xattr_rec(self, file):
627 data = vint.read_bvec(file)
628 memfile = StringIO(data)
630 for i in range(vint.read_vuint(memfile)):
631 key = vint.read_bvec(memfile)
632 value = vint.read_bvec(memfile)
633 result.append((key, value))
634 self.linux_xattr = result
636 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
639 add_error("%s: can't restore xattr; xattr support missing.\n"
642 if not self.linux_xattr:
645 existing_xattrs = set(xattr.list(path, nofollow=True))
647 if e.errno == errno.EACCES:
648 raise ApplyError('xattr.set: %s' % e)
651 for k, v in self.linux_xattr:
652 if k not in existing_xattrs \
653 or v != xattr.get(path, k, nofollow=True):
655 xattr.set(path, k, v, nofollow=True)
657 if e.errno == errno.EPERM \
658 or e.errno == errno.EOPNOTSUPP:
659 raise ApplyError('xattr.set: %s' % e)
662 existing_xattrs -= frozenset([k])
663 for k in existing_xattrs:
665 xattr.remove(path, k, nofollow=True)
667 if e.errno == errno.EPERM:
668 raise ApplyError('xattr.remove: %s' % e)
673 self.mode = self.uid = self.gid = self.user = self.group = None
674 self.atime = self.mtime = self.ctime = None
678 self.symlink_target = None
679 self.hardlink_target = None
680 self.linux_attr = None
681 self.linux_xattr = None
682 self.posix1e_acl = None
685 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
687 result += ' path:' + repr(self.path)
689 result += ' mode:' + repr(xstat.mode_str(self.mode)
690 + '(%s)' % hex(self.mode))
692 result += ' uid:' + str(self.uid)
694 result += ' gid:' + str(self.gid)
696 result += ' user:' + repr(self.user)
698 result += ' group:' + repr(self.group)
700 result += ' size:' + repr(self.size)
701 for name, val in (('atime', self.atime),
702 ('mtime', self.mtime),
703 ('ctime', self.ctime)):
706 time.strftime('%Y-%m-%d %H:%M %z',
707 time.gmtime(xstat.fstime_floor_secs(val))))
709 return ''.join(result)
711 def write(self, port, include_path=True):
712 records = include_path and [(_rec_tag_path, self._encode_path())] or []
713 records.extend([(_rec_tag_common_v2, self._encode_common()),
714 (_rec_tag_symlink_target,
715 self._encode_symlink_target()),
716 (_rec_tag_hardlink_target,
717 self._encode_hardlink_target()),
718 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
719 (_rec_tag_linux_attr, self._encode_linux_attr()),
720 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
721 for tag, data in records:
723 vint.write_vuint(port, tag)
724 vint.write_bvec(port, data)
725 vint.write_vuint(port, _rec_tag_end)
727 def encode(self, include_path=True):
729 self.write(port, include_path)
730 return port.getvalue()
734 # This method should either return a valid Metadata object,
735 # return None if there was no information at all (just a
736 # _rec_tag_end), throw EOFError if there was nothing at all to
737 # read, or throw an Exception if a valid object could not be
739 tag = vint.read_vuint(port)
740 if tag == _rec_tag_end:
742 try: # From here on, EOF is an error.
744 while True: # only exit is error (exception) or _rec_tag_end
745 if tag == _rec_tag_path:
746 result._load_path_rec(port)
747 elif tag == _rec_tag_common_v2:
748 result._load_common_rec(port)
749 elif tag == _rec_tag_symlink_target:
750 result._load_symlink_target_rec(port)
751 elif tag == _rec_tag_hardlink_target:
752 result._load_hardlink_target_rec(port)
753 elif tag == _rec_tag_posix1e_acl:
754 result._load_posix1e_acl_rec(port)
755 elif tag == _rec_tag_linux_attr:
756 result._load_linux_attr_rec(port)
757 elif tag == _rec_tag_linux_xattr:
758 result._load_linux_xattr_rec(port)
759 elif tag == _rec_tag_end:
761 elif tag == _rec_tag_common: # Should be very rare.
762 result._load_common_rec(port, legacy_format = True)
763 else: # unknown record
765 tag = vint.read_vuint(port)
767 raise Exception("EOF while reading Metadata")
770 return stat.S_ISDIR(self.mode)
772 def create_path(self, path, create_symlinks=True):
773 self._create_via_common_rec(path, create_symlinks=create_symlinks)
775 def apply_to_path(self, path=None, restore_numeric_ids=False):
776 # apply metadata to path -- file must exist
780 raise Exception('Metadata.apply_to_path() called with no path')
781 if not self._recognized_file_type():
782 add_error('not applying metadata to "%s"' % path
783 + ' with unrecognized mode "0x%x"\n' % self.mode)
785 num_ids = restore_numeric_ids
786 for apply_metadata in (self._apply_common_rec,
787 self._apply_posix1e_acl_rec,
788 self._apply_linux_attr_rec,
789 self._apply_linux_xattr_rec):
791 apply_metadata(path, restore_numeric_ids=num_ids)
792 except ApplyError, e:
795 def same_file(self, other):
796 """Compare this to other for equivalency. Return true if
797 their information implies they could represent the same file
798 on disk, in the hardlink sense. Assume they're both regular
800 return self._same_common(other) \
801 and self._same_hardlink_target(other) \
802 and self._same_posix1e_acl(other) \
803 and self._same_linux_attr(other) \
804 and self._same_linux_xattr(other)
807 def from_path(path, statinfo=None, archive_path=None,
808 save_symlinks=True, hardlink_target=None):
810 result.path = archive_path
811 st = statinfo or xstat.lstat(path)
812 result.size = st.st_size
813 result._add_common(path, st)
815 result._add_symlink_target(path, st)
816 result._add_hardlink_target(hardlink_target)
817 result._add_posix1e_acl(path, st)
818 result._add_linux_attr(path, st)
819 result._add_linux_xattr(path, st)
823 def save_tree(output_file, paths,
829 # Issue top-level rewrite warnings.
831 safe_path = _clean_up_path_for_archive(path)
832 if safe_path != path:
833 log('archiving "%s" as "%s"\n' % (path, safe_path))
837 safe_path = _clean_up_path_for_archive(p)
839 if stat.S_ISDIR(st.st_mode):
841 m = from_path(p, statinfo=st, archive_path=safe_path,
842 save_symlinks=save_symlinks)
844 print >> sys.stderr, m.path
845 m.write(output_file, include_path=write_paths)
847 start_dir = os.getcwd()
849 for (p, st) in recursive_dirlist(paths, xdev=xdev):
850 dirlist_dir = os.getcwd()
852 safe_path = _clean_up_path_for_archive(p)
853 m = from_path(p, statinfo=st, archive_path=safe_path,
854 save_symlinks=save_symlinks)
856 print >> sys.stderr, m.path
857 m.write(output_file, include_path=write_paths)
858 os.chdir(dirlist_dir)
863 def _set_up_path(meta, create_symlinks=True):
864 # Allow directories to exist as a special case -- might have
865 # been created by an earlier longer path.
869 parent = os.path.dirname(meta.path)
872 meta.create_path(meta.path, create_symlinks=create_symlinks)
875 all_fields = frozenset(['path',
892 def summary_str(meta, numeric_ids = False, human_readable = False):
893 mode_val = xstat.mode_str(meta.mode)
895 if numeric_ids or not user_val:
896 user_val = str(meta.uid)
897 group_val = meta.group
898 if numeric_ids or not group_val:
899 group_val = str(meta.gid)
900 size_or_dev_val = '-'
901 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
902 size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
903 elif meta.size != None:
904 size_or_dev_val = meta.size
906 size_or_dev_val = format_filesize(meta.size)
907 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
908 time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
909 path_val = meta.path or ''
910 if stat.S_ISLNK(meta.mode):
911 path_val += ' -> ' + meta.symlink_target
912 return '%-10s %-11s %11s %16s %s' % (mode_val,
913 user_val + "/" + group_val,
919 def detailed_str(meta, fields = None):
920 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
921 # 0", "link-target:", etc.
927 path = meta.path or ''
928 result.append('path: ' + path)
930 result.append('mode: %s (%s)' % (oct(meta.mode),
931 xstat.mode_str(meta.mode)))
932 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
933 result.append('link-target: ' + meta.symlink_target)
936 result.append('rdev: %d,%d' % (os.major(meta.rdev),
937 os.minor(meta.rdev)))
939 result.append('rdev: 0')
940 if 'size' in fields and meta.size:
941 result.append('size: ' + str(meta.size))
943 result.append('uid: ' + str(meta.uid))
945 result.append('gid: ' + str(meta.gid))
947 result.append('user: ' + meta.user)
948 if 'group' in fields:
949 result.append('group: ' + meta.group)
950 if 'atime' in fields:
951 # If we don't have xstat.lutime, that means we have to use
952 # utime(), and utime() has no way to set the mtime/atime of a
953 # symlink. Thus, the mtime/atime of a symlink is meaningless,
954 # so let's not report it. (That way scripts comparing
955 # before/after won't trigger.)
956 if xstat.lutime or not stat.S_ISLNK(meta.mode):
957 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
959 result.append('atime: 0')
960 if 'mtime' in fields:
961 if xstat.lutime or not stat.S_ISLNK(meta.mode):
962 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
964 result.append('mtime: 0')
965 if 'ctime' in fields:
966 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
967 if 'linux-attr' in fields and meta.linux_attr:
968 result.append('linux-attr: ' + hex(meta.linux_attr))
969 if 'linux-xattr' in fields and meta.linux_xattr:
970 for name, value in meta.linux_xattr:
971 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
972 if 'posix1e-acl' in fields and meta.posix1e_acl:
973 acl = meta.posix1e_acl[0]
974 result.append('posix1e-acl: ' + acl + '\n')
975 if stat.S_ISDIR(meta.mode):
976 def_acl = meta.posix1e_acl[2]
977 result.append('posix1e-acl-default: ' + def_acl + '\n')
978 return '\n'.join(result)
981 class _ArchiveIterator:
984 return Metadata.read(self._file)
986 raise StopIteration()
991 def __init__(self, file):
995 def display_archive(file):
998 for meta in _ArchiveIterator(file):
1001 print detailed_str(meta)
1004 for meta in _ArchiveIterator(file):
1005 print summary_str(meta)
1007 for meta in _ArchiveIterator(file):
1009 print >> sys.stderr, \
1010 'bup: no metadata path, but asked to only display path', \
1011 '(increase verbosity?)'
1016 def start_extract(file, create_symlinks=True):
1017 for meta in _ArchiveIterator(file):
1018 if not meta: # Hit end record.
1021 print >> sys.stderr, meta.path
1022 xpath = _clean_up_extract_path(meta.path)
1024 add_error(Exception('skipping risky path "%s"' % meta.path))
1027 _set_up_path(meta, create_symlinks=create_symlinks)
1030 def finish_extract(file, restore_numeric_ids=False):
1032 for meta in _ArchiveIterator(file):
1033 if not meta: # Hit end record.
1035 xpath = _clean_up_extract_path(meta.path)
1037 add_error(Exception('skipping risky path "%s"' % dir.path))
1039 if os.path.isdir(meta.path):
1040 all_dirs.append(meta)
1043 print >> sys.stderr, meta.path
1044 meta.apply_to_path(path=xpath,
1045 restore_numeric_ids=restore_numeric_ids)
1046 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1047 for dir in all_dirs:
1048 # Don't need to check xpath -- won't be in all_dirs if not OK.
1049 xpath = _clean_up_extract_path(dir.path)
1051 print >> sys.stderr, dir.path
1052 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1055 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1056 # For now, just store all the directories and handle them last,
1059 for meta in _ArchiveIterator(file):
1060 if not meta: # Hit end record.
1062 xpath = _clean_up_extract_path(meta.path)
1064 add_error(Exception('skipping risky path "%s"' % meta.path))
1068 print >> sys.stderr, '+', meta.path
1069 _set_up_path(meta, create_symlinks=create_symlinks)
1070 if os.path.isdir(meta.path):
1071 all_dirs.append(meta)
1074 print >> sys.stderr, '=', meta.path
1075 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1076 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1077 for dir in all_dirs:
1078 # Don't need to check xpath -- won't be in all_dirs if not OK.
1079 xpath = _clean_up_extract_path(dir.path)
1081 print >> sys.stderr, '=', xpath
1082 # Shouldn't have to check for risky paths here (omitted above).
1083 dir.apply_to_path(path=dir.path,
1084 restore_numeric_ids=restore_numeric_ids)