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, struct
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 # See the bup_get_linux_file_attr() comments.
48 _suppress_linux_file_attr = \
49 sys.byteorder == 'big' and struct.calcsize('@l') > struct.calcsize('@i')
51 def check_linux_file_attr_api():
52 global get_linux_file_attr, set_linux_file_attr
53 if not (get_linux_file_attr or set_linux_file_attr):
55 if _suppress_linux_file_attr:
56 log('Warning: Linux attr support disabled (see "bup help index").\n')
57 get_linux_file_attr = set_linux_file_attr = None
60 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
62 # Q: Consider hardlink support?
63 # Q: Is it OK to store raw linux attr (chattr) flags?
64 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
65 # Q: Is the application of posix1e has_extended() correct?
66 # Q: Is one global --numeric-ids argument sufficient?
67 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
68 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
70 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
71 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
72 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
73 # FIXME: Consider pack('vvvvsss', ...) optimization.
77 # osx (varies between hfs and hfs+):
78 # type - regular dir char block fifo socket ...
79 # perms - rwxrwxrwxsgt
80 # times - ctime atime mtime
83 # hard-link-info (hfs+ only)
86 # attributes-osx see chflags
92 # type - regular dir ...
93 # times - creation, modification, posix change, access
96 # attributes - see attrib
98 # forks (alternate data streams)
102 # type - regular dir ...
103 # perms - rwxrwxrwx (maybe - see wikipedia)
104 # times - creation, modification, access
105 # attributes - see attrib
109 _have_lchmod = hasattr(os, 'lchmod')
112 def _clean_up_path_for_archive(p):
113 # Not the most efficient approach.
116 # Take everything after any '/../'.
117 pos = result.rfind('/../')
119 result = result[result.rfind('/../') + 4:]
121 # Take everything after any remaining '../'.
122 if result.startswith("../"):
125 # Remove any '/./' sequences.
126 pos = result.find('/./')
128 result = result[0:pos] + '/' + result[pos + 3:]
129 pos = result.find('/./')
131 # Remove any leading '/'s.
132 result = result.lstrip('/')
134 # Replace '//' with '/' everywhere.
135 pos = result.find('//')
137 result = result[0:pos] + '/' + result[pos + 2:]
138 pos = result.find('//')
140 # Take everything after any remaining './'.
141 if result.startswith('./'):
144 # Take everything before any remaining '/.'.
145 if result.endswith('/.'):
148 if result == '' or result.endswith('/..'):
155 if p.startswith('/'):
157 if p.find('/../') != -1:
159 if p.startswith('../'):
161 if p.endswith('/..'):
166 def _clean_up_extract_path(p):
167 result = p.lstrip('/')
170 elif _risky_path(result):
176 # These tags are currently conceptually private to Metadata, and they
177 # must be unique, and must *never* be changed.
180 _rec_tag_common = 2 # times, user, group, type, perms, etc. (legacy/broken)
181 _rec_tag_symlink_target = 3
182 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
183 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
184 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
185 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
186 _rec_tag_hardlink_target = 8 # hard link target path
187 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
190 class ApplyError(Exception):
191 # Thrown when unable to apply any given bit of metadata to a path.
196 # Metadata is stored as a sequence of tagged binary records. Each
197 # record will have some subset of add, encode, load, create, and
198 # apply methods, i.e. _add_foo...
200 # We do allow an "empty" object as a special case, i.e. no
201 # records. One can be created by trying to write Metadata(), and
202 # for such an object, read() will return None. This is used by
203 # "bup save", for example, as a placeholder in cases where
206 # NOTE: if any relevant fields are added or removed, be sure to
207 # update same_file() below.
211 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
212 # must be non-negative and < 10**9.
214 def _add_common(self, path, st):
215 assert(st.st_uid >= 0)
216 assert(st.st_gid >= 0)
219 self.atime = st.st_atime
220 self.mtime = st.st_mtime
221 self.ctime = st.st_ctime
222 self.user = self.group = ''
223 entry = pwd_from_uid(st.st_uid)
225 self.user = entry.pw_name
226 entry = grp_from_gid(st.st_gid)
228 self.group = entry.gr_name
229 self.mode = st.st_mode
230 # Only collect st_rdev if we might need it for a mknod()
231 # during restore. On some platforms (i.e. kFreeBSD), it isn't
232 # stable for other file types. For example "cp -a" will
233 # change it for a plain file.
234 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
235 self.rdev = st.st_rdev
239 def _same_common(self, other):
240 """Return true or false to indicate similarity in the hardlink sense."""
241 return self.uid == other.uid \
242 and self.gid == other.gid \
243 and self.rdev == other.rdev \
244 and self.mtime == other.mtime \
245 and self.ctime == other.ctime \
246 and self.user == other.user \
247 and self.group == other.group
249 def _encode_common(self):
252 atime = xstat.nsecs_to_timespec(self.atime)
253 mtime = xstat.nsecs_to_timespec(self.mtime)
254 ctime = xstat.nsecs_to_timespec(self.ctime)
255 result = vint.pack('vvsvsvvVvVvV',
270 def _load_common_rec(self, port, legacy_format=False):
271 unpack_fmt = 'vvsvsvvVvVvV'
273 unpack_fmt = 'VVsVsVvVvVvV'
274 data = vint.read_bvec(port)
286 ctime_ns) = vint.unpack(unpack_fmt, data)
287 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
288 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
289 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
291 def _recognized_file_type(self):
292 return stat.S_ISREG(self.mode) \
293 or stat.S_ISDIR(self.mode) \
294 or stat.S_ISCHR(self.mode) \
295 or stat.S_ISBLK(self.mode) \
296 or stat.S_ISFIFO(self.mode) \
297 or stat.S_ISSOCK(self.mode) \
298 or stat.S_ISLNK(self.mode)
300 def _create_via_common_rec(self, path, create_symlinks=True):
302 raise ApplyError('no metadata - cannot create path ' + path)
304 # If the path already exists and is a dir, try rmdir.
305 # If the path already exists and is anything else, try unlink.
308 st = xstat.lstat(path)
310 if e.errno != errno.ENOENT:
313 if stat.S_ISDIR(st.st_mode):
317 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
318 msg = 'refusing to overwrite non-empty dir ' + path
324 if stat.S_ISREG(self.mode):
325 assert(self._recognized_file_type())
326 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
328 elif stat.S_ISDIR(self.mode):
329 assert(self._recognized_file_type())
331 elif stat.S_ISCHR(self.mode):
332 assert(self._recognized_file_type())
333 os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
334 elif stat.S_ISBLK(self.mode):
335 assert(self._recognized_file_type())
336 os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
337 elif stat.S_ISFIFO(self.mode):
338 assert(self._recognized_file_type())
339 os.mknod(path, 0600 | stat.S_IFIFO)
340 elif stat.S_ISSOCK(self.mode):
342 os.mknod(path, 0600 | stat.S_IFSOCK)
344 if e.errno in (errno.EINVAL, errno.EPERM):
345 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
349 elif stat.S_ISLNK(self.mode):
350 assert(self._recognized_file_type())
351 if self.symlink_target and create_symlinks:
352 # on MacOS, symlink() permissions depend on umask, and there's
353 # no way to chown a symlink after creating it, so we have to
355 oldumask = os.umask((self.mode & 0777) ^ 0777)
357 os.symlink(self.symlink_target, path)
360 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
362 assert(not self._recognized_file_type())
363 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
366 def _apply_common_rec(self, path, restore_numeric_ids=False):
368 raise ApplyError('no metadata - cannot apply to ' + path)
370 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
371 # EACCES errors at this stage are fatal for the current path.
372 if lutime and stat.S_ISLNK(self.mode):
374 lutime(path, (self.atime, self.mtime))
376 if e.errno == errno.EACCES:
377 raise ApplyError('lutime: %s' % e)
382 utime(path, (self.atime, self.mtime))
384 if e.errno == errno.EACCES:
385 raise ApplyError('utime: %s' % e)
389 uid = gid = -1 # By default, do nothing.
393 if not restore_numeric_ids:
394 if self.uid != 0 and self.user:
395 entry = pwd_from_name(self.user)
398 if self.gid != 0 and self.group:
399 entry = grp_from_name(self.group)
402 else: # not superuser - only consider changing the group/gid
403 user_gids = os.getgroups()
404 if self.gid in user_gids:
406 if not restore_numeric_ids and self.gid != 0:
407 # The grp might not exist on the local system.
408 grps = filter(None, [grp_from_gid(x) for x in user_gids])
409 if self.group in [x.gr_name for x in grps]:
410 g = grp_from_name(self.group)
414 if uid != -1 or gid != -1:
416 os.lchown(path, uid, gid)
418 if e.errno == errno.EPERM:
419 add_error('lchown: %s' % e)
420 elif sys.platform.startswith('cygwin') \
421 and e.errno == errno.EINVAL:
422 add_error('lchown: unknown uid/gid (%d/%d) for %s'
429 os.lchmod(path, stat.S_IMODE(self.mode))
430 except errno.ENOSYS: # Function not implemented
432 elif not stat.S_ISLNK(self.mode):
433 os.chmod(path, stat.S_IMODE(self.mode))
438 def _encode_path(self):
440 return vint.pack('s', self.path)
444 def _load_path_rec(self, port):
445 self.path = vint.unpack('s', vint.read_bvec(port))[0]
450 def _add_symlink_target(self, path, st):
452 if stat.S_ISLNK(st.st_mode):
453 self.symlink_target = os.readlink(path)
455 add_error('readlink: %s' % e)
457 def _encode_symlink_target(self):
458 return self.symlink_target
460 def _load_symlink_target_rec(self, port):
461 self.symlink_target = vint.read_bvec(port)
466 def _add_hardlink_target(self, target):
467 self.hardlink_target = target
469 def _same_hardlink_target(self, other):
470 """Return true or false to indicate similarity in the hardlink sense."""
471 return self.hardlink_target == other.hardlink_target
473 def _encode_hardlink_target(self):
474 return self.hardlink_target
476 def _load_hardlink_target_rec(self, port):
477 self.hardlink_target = vint.read_bvec(port)
480 ## POSIX1e ACL records
482 # Recorded as a list:
483 # [txt_id_acl, num_id_acl]
484 # or, if a directory:
485 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
486 # The numeric/text distinction only matters when reading/restoring
488 def _add_posix1e_acl(self, path, st):
489 if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
491 if not stat.S_ISLNK(st.st_mode):
495 if posix1e.has_extended(path):
496 acl = posix1e.ACL(file=path)
497 acls = [acl, acl] # txt and num are the same
498 if stat.S_ISDIR(st.st_mode):
499 def_acl = posix1e.ACL(filedef=path)
500 def_acls = [def_acl, def_acl]
501 except EnvironmentError, e:
502 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
505 txt_flags = posix1e.TEXT_ABBREVIATE
506 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
507 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
508 acls[1].to_any_text('', '\n', num_flags)]
510 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
511 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
512 self.posix1e_acl = acl_rep
514 def _same_posix1e_acl(self, other):
515 """Return true or false to indicate similarity in the hardlink sense."""
516 return self.posix1e_acl == other.posix1e_acl
518 def _encode_posix1e_acl(self):
519 # Encode as two strings (w/default ACL string possibly empty).
521 acls = self.posix1e_acl
523 acls.extend(['', ''])
524 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
528 def _load_posix1e_acl_rec(self, port):
529 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
531 acl_rep = acl_rep[:2]
532 self.posix1e_acl = acl_rep
534 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
535 def apply_acl(acl_rep, kind):
537 acl = posix1e.ACL(text = acl_rep)
540 # pylibacl appears to return an IOError with errno
541 # set to 0 if a group referred to by the ACL rep
542 # doesn't exist on the current system.
543 raise ApplyError("POSIX1e ACL: can't create %r for %r"
548 acl.applyto(path, kind)
550 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
551 raise ApplyError('POSIX1e ACL applyto: %s' % e)
557 add_error("%s: can't restore ACLs; posix1e support missing.\n"
561 acls = self.posix1e_acl
563 if restore_numeric_ids:
564 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
566 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
567 if restore_numeric_ids:
568 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
570 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
573 ## Linux attributes (lsattr(1), chattr(1))
575 def _add_linux_attr(self, path, st):
576 check_linux_file_attr_api()
577 if not get_linux_file_attr: return
578 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
580 attr = get_linux_file_attr(path)
582 self.linux_attr = attr
584 if e.errno == errno.EACCES:
585 add_error('read Linux attr: %s' % e)
586 elif e.errno in (errno.ENOTTY, errno.ENOSYS, errno.EOPNOTSUPP):
587 # Assume filesystem doesn't support attrs.
592 def _same_linux_attr(self, other):
593 """Return true or false to indicate similarity in the hardlink sense."""
594 return self.linux_attr == other.linux_attr
596 def _encode_linux_attr(self):
598 return vint.pack('V', self.linux_attr)
602 def _load_linux_attr_rec(self, port):
603 data = vint.read_bvec(port)
604 self.linux_attr = vint.unpack('V', data)[0]
606 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
608 check_linux_file_attr_api()
609 if not set_linux_file_attr:
610 add_error("%s: can't restore linuxattrs: "
611 "linuxattr support missing.\n" % path)
614 set_linux_file_attr(path, self.linux_attr)
616 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS,
618 raise ApplyError('Linux chattr: %s (0x%s)'
619 % (e, hex(self.linux_attr)))
624 ## Linux extended attributes (getfattr(1), setfattr(1))
626 def _add_linux_xattr(self, path, st):
629 self.linux_xattr = xattr.get_all(path, nofollow=True)
630 except EnvironmentError, e:
631 if e.errno != errno.EOPNOTSUPP:
634 def _same_linux_xattr(self, other):
635 """Return true or false to indicate similarity in the hardlink sense."""
636 return self.linux_xattr == other.linux_xattr
638 def _encode_linux_xattr(self):
640 result = vint.pack('V', len(self.linux_xattr))
641 for name, value in self.linux_xattr:
642 result += vint.pack('ss', name, value)
647 def _load_linux_xattr_rec(self, file):
648 data = vint.read_bvec(file)
649 memfile = StringIO(data)
651 for i in range(vint.read_vuint(memfile)):
652 key = vint.read_bvec(memfile)
653 value = vint.read_bvec(memfile)
654 result.append((key, value))
655 self.linux_xattr = result
657 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
660 add_error("%s: can't restore xattr; xattr support missing.\n"
663 if not self.linux_xattr:
666 existing_xattrs = set(xattr.list(path, nofollow=True))
668 if e.errno == errno.EACCES:
669 raise ApplyError('xattr.set %r: %s' % (path, e))
672 for k, v in self.linux_xattr:
673 if k not in existing_xattrs \
674 or v != xattr.get(path, k, nofollow=True):
676 xattr.set(path, k, v, nofollow=True)
678 if e.errno == errno.EPERM \
679 or e.errno == errno.EOPNOTSUPP:
680 raise ApplyError('xattr.set %r: %s' % (path, e))
683 existing_xattrs -= frozenset([k])
684 for k in existing_xattrs:
686 xattr.remove(path, k, nofollow=True)
688 if e.errno == errno.EPERM:
689 raise ApplyError('xattr.remove %r: %s' % (path, e))
694 self.mode = self.uid = self.gid = self.user = self.group = None
695 self.atime = self.mtime = self.ctime = None
699 self.symlink_target = None
700 self.hardlink_target = None
701 self.linux_attr = None
702 self.linux_xattr = None
703 self.posix1e_acl = None
706 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
708 result += ' path:' + repr(self.path)
710 result += ' mode:' + repr(xstat.mode_str(self.mode)
711 + '(%s)' % hex(self.mode))
713 result += ' uid:' + str(self.uid)
715 result += ' gid:' + str(self.gid)
717 result += ' user:' + repr(self.user)
719 result += ' group:' + repr(self.group)
721 result += ' size:' + repr(self.size)
722 for name, val in (('atime', self.atime),
723 ('mtime', self.mtime),
724 ('ctime', self.ctime)):
727 time.strftime('%Y-%m-%d %H:%M %z',
728 time.gmtime(xstat.fstime_floor_secs(val))))
730 return ''.join(result)
732 def write(self, port, include_path=True):
733 records = include_path and [(_rec_tag_path, self._encode_path())] or []
734 records.extend([(_rec_tag_common_v2, self._encode_common()),
735 (_rec_tag_symlink_target,
736 self._encode_symlink_target()),
737 (_rec_tag_hardlink_target,
738 self._encode_hardlink_target()),
739 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
740 (_rec_tag_linux_attr, self._encode_linux_attr()),
741 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
742 for tag, data in records:
744 vint.write_vuint(port, tag)
745 vint.write_bvec(port, data)
746 vint.write_vuint(port, _rec_tag_end)
748 def encode(self, include_path=True):
750 self.write(port, include_path)
751 return port.getvalue()
755 # This method should either return a valid Metadata object,
756 # return None if there was no information at all (just a
757 # _rec_tag_end), throw EOFError if there was nothing at all to
758 # read, or throw an Exception if a valid object could not be
760 tag = vint.read_vuint(port)
761 if tag == _rec_tag_end:
763 try: # From here on, EOF is an error.
765 while True: # only exit is error (exception) or _rec_tag_end
766 if tag == _rec_tag_path:
767 result._load_path_rec(port)
768 elif tag == _rec_tag_common_v2:
769 result._load_common_rec(port)
770 elif tag == _rec_tag_symlink_target:
771 result._load_symlink_target_rec(port)
772 elif tag == _rec_tag_hardlink_target:
773 result._load_hardlink_target_rec(port)
774 elif tag == _rec_tag_posix1e_acl:
775 result._load_posix1e_acl_rec(port)
776 elif tag == _rec_tag_linux_attr:
777 result._load_linux_attr_rec(port)
778 elif tag == _rec_tag_linux_xattr:
779 result._load_linux_xattr_rec(port)
780 elif tag == _rec_tag_end:
782 elif tag == _rec_tag_common: # Should be very rare.
783 result._load_common_rec(port, legacy_format = True)
784 else: # unknown record
786 tag = vint.read_vuint(port)
788 raise Exception("EOF while reading Metadata")
791 return stat.S_ISDIR(self.mode)
793 def create_path(self, path, create_symlinks=True):
794 self._create_via_common_rec(path, create_symlinks=create_symlinks)
796 def apply_to_path(self, path=None, restore_numeric_ids=False):
797 # apply metadata to path -- file must exist
801 raise Exception('Metadata.apply_to_path() called with no path')
802 if not self._recognized_file_type():
803 add_error('not applying metadata to "%s"' % path
804 + ' with unrecognized mode "0x%x"\n' % self.mode)
806 num_ids = restore_numeric_ids
807 for apply_metadata in (self._apply_common_rec,
808 self._apply_posix1e_acl_rec,
809 self._apply_linux_attr_rec,
810 self._apply_linux_xattr_rec):
812 apply_metadata(path, restore_numeric_ids=num_ids)
813 except ApplyError, e:
816 def same_file(self, other):
817 """Compare this to other for equivalency. Return true if
818 their information implies they could represent the same file
819 on disk, in the hardlink sense. Assume they're both regular
821 return self._same_common(other) \
822 and self._same_hardlink_target(other) \
823 and self._same_posix1e_acl(other) \
824 and self._same_linux_attr(other) \
825 and self._same_linux_xattr(other)
828 def from_path(path, statinfo=None, archive_path=None,
829 save_symlinks=True, hardlink_target=None):
831 result.path = archive_path
832 st = statinfo or xstat.lstat(path)
833 result.size = st.st_size
834 result._add_common(path, st)
836 result._add_symlink_target(path, st)
837 result._add_hardlink_target(hardlink_target)
838 result._add_posix1e_acl(path, st)
839 result._add_linux_attr(path, st)
840 result._add_linux_xattr(path, st)
844 def save_tree(output_file, paths,
850 # Issue top-level rewrite warnings.
852 safe_path = _clean_up_path_for_archive(path)
853 if safe_path != path:
854 log('archiving "%s" as "%s"\n' % (path, safe_path))
858 safe_path = _clean_up_path_for_archive(p)
860 if stat.S_ISDIR(st.st_mode):
862 m = from_path(p, statinfo=st, archive_path=safe_path,
863 save_symlinks=save_symlinks)
865 print >> sys.stderr, m.path
866 m.write(output_file, include_path=write_paths)
868 start_dir = os.getcwd()
870 for (p, st) in recursive_dirlist(paths, xdev=xdev):
871 dirlist_dir = os.getcwd()
873 safe_path = _clean_up_path_for_archive(p)
874 m = from_path(p, statinfo=st, archive_path=safe_path,
875 save_symlinks=save_symlinks)
877 print >> sys.stderr, m.path
878 m.write(output_file, include_path=write_paths)
879 os.chdir(dirlist_dir)
884 def _set_up_path(meta, create_symlinks=True):
885 # Allow directories to exist as a special case -- might have
886 # been created by an earlier longer path.
890 parent = os.path.dirname(meta.path)
893 meta.create_path(meta.path, create_symlinks=create_symlinks)
896 all_fields = frozenset(['path',
913 def summary_str(meta, numeric_ids = False, classification = None,
914 human_readable = False):
916 """Return a string containing the "ls -l" style listing for meta.
917 Classification may be "all", "type", or None."""
918 user_str = group_str = size_or_dev_str = '?'
919 symlink_target = None
922 mode_str = xstat.mode_str(meta.mode)
923 symlink_target = meta.symlink_target
924 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
925 mtime_str = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
926 if meta.user and not numeric_ids:
928 elif meta.uid != None:
929 user_str = str(meta.uid)
930 if meta.group and not numeric_ids:
931 group_str = meta.group
932 elif meta.gid != None:
933 group_str = str(meta.gid)
934 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
936 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
938 elif meta.size != None:
940 size_or_dev_str = format_filesize(meta.size)
942 size_or_dev_str = str(meta.size)
944 size_or_dev_str = '-'
946 classification_str = \
947 xstat.classification_str(meta.mode, classification == 'all')
950 mtime_str = '????-??-?? ??:??'
951 classification_str = '?'
955 name += classification_str
957 name += ' -> ' + meta.symlink_target
959 return '%-10s %-11s %11s %16s %s' % (mode_str,
960 user_str + "/" + group_str,
966 def detailed_str(meta, fields = None):
967 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
968 # 0", "link-target:", etc.
974 path = meta.path or ''
975 result.append('path: ' + path)
977 result.append('mode: %s (%s)' % (oct(meta.mode),
978 xstat.mode_str(meta.mode)))
979 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
980 result.append('link-target: ' + meta.symlink_target)
983 result.append('rdev: %d,%d' % (os.major(meta.rdev),
984 os.minor(meta.rdev)))
986 result.append('rdev: 0')
987 if 'size' in fields and meta.size:
988 result.append('size: ' + str(meta.size))
990 result.append('uid: ' + str(meta.uid))
992 result.append('gid: ' + str(meta.gid))
994 result.append('user: ' + meta.user)
995 if 'group' in fields:
996 result.append('group: ' + meta.group)
997 if 'atime' in fields:
998 # If we don't have xstat.lutime, that means we have to use
999 # utime(), and utime() has no way to set the mtime/atime of a
1000 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1001 # so let's not report it. (That way scripts comparing
1002 # before/after won't trigger.)
1003 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1004 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1006 result.append('atime: 0')
1007 if 'mtime' in fields:
1008 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1009 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1011 result.append('mtime: 0')
1012 if 'ctime' in fields:
1013 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1014 if 'linux-attr' in fields and meta.linux_attr:
1015 result.append('linux-attr: ' + hex(meta.linux_attr))
1016 if 'linux-xattr' in fields and meta.linux_xattr:
1017 for name, value in meta.linux_xattr:
1018 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1019 if 'posix1e-acl' in fields and meta.posix1e_acl:
1020 acl = meta.posix1e_acl[0]
1021 result.append('posix1e-acl: ' + acl + '\n')
1022 if stat.S_ISDIR(meta.mode):
1023 def_acl = meta.posix1e_acl[2]
1024 result.append('posix1e-acl-default: ' + def_acl + '\n')
1025 return '\n'.join(result)
1028 class _ArchiveIterator:
1031 return Metadata.read(self._file)
1033 raise StopIteration()
1038 def __init__(self, file):
1042 def display_archive(file):
1045 for meta in _ArchiveIterator(file):
1048 print detailed_str(meta)
1051 for meta in _ArchiveIterator(file):
1052 print summary_str(meta)
1054 for meta in _ArchiveIterator(file):
1056 print >> sys.stderr, \
1057 'bup: no metadata path, but asked to only display path', \
1058 '(increase verbosity?)'
1063 def start_extract(file, create_symlinks=True):
1064 for meta in _ArchiveIterator(file):
1065 if not meta: # Hit end record.
1068 print >> sys.stderr, meta.path
1069 xpath = _clean_up_extract_path(meta.path)
1071 add_error(Exception('skipping risky path "%s"' % meta.path))
1074 _set_up_path(meta, create_symlinks=create_symlinks)
1077 def finish_extract(file, restore_numeric_ids=False):
1079 for meta in _ArchiveIterator(file):
1080 if not meta: # Hit end record.
1082 xpath = _clean_up_extract_path(meta.path)
1084 add_error(Exception('skipping risky path "%s"' % dir.path))
1086 if os.path.isdir(meta.path):
1087 all_dirs.append(meta)
1090 print >> sys.stderr, meta.path
1091 meta.apply_to_path(path=xpath,
1092 restore_numeric_ids=restore_numeric_ids)
1093 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1094 for dir in all_dirs:
1095 # Don't need to check xpath -- won't be in all_dirs if not OK.
1096 xpath = _clean_up_extract_path(dir.path)
1098 print >> sys.stderr, dir.path
1099 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1102 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1103 # For now, just store all the directories and handle them last,
1106 for meta in _ArchiveIterator(file):
1107 if not meta: # Hit end record.
1109 xpath = _clean_up_extract_path(meta.path)
1111 add_error(Exception('skipping risky path "%s"' % meta.path))
1115 print >> sys.stderr, '+', meta.path
1116 _set_up_path(meta, create_symlinks=create_symlinks)
1117 if os.path.isdir(meta.path):
1118 all_dirs.append(meta)
1121 print >> sys.stderr, '=', meta.path
1122 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1123 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1124 for dir in all_dirs:
1125 # Don't need to check xpath -- won't be in all_dirs if not OK.
1126 xpath = _clean_up_extract_path(dir.path)
1128 print >> sys.stderr, '=', xpath
1129 # Shouldn't have to check for risky paths here (omitted above).
1130 dir.apply_to_path(path=dir.path,
1131 restore_numeric_ids=restore_numeric_ids)