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 _suppress_linux_file_attr = \
48 sys.byteorder == 'big' and struct.calcsize('=l') > struct.calcsize('=i')
50 def check_linux_file_attr_api():
51 global get_linux_file_attr, set_linux_file_attr
52 if not (get_linux_file_attr or set_linux_file_attr):
54 if _suppress_linux_file_attr:
55 log('Warning: Linux attr support disabled (see "bup help index").\n')
56 get_linux_file_attr = set_linux_file_attr = None
59 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
61 # Q: Consider hardlink support?
62 # Q: Is it OK to store raw linux attr (chattr) flags?
63 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
64 # Q: Is the application of posix1e has_extended() correct?
65 # Q: Is one global --numeric-ids argument sufficient?
66 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
67 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
69 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
70 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
71 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
72 # FIXME: Consider pack('vvvvsss', ...) optimization.
76 # osx (varies between hfs and hfs+):
77 # type - regular dir char block fifo socket ...
78 # perms - rwxrwxrwxsgt
79 # times - ctime atime mtime
82 # hard-link-info (hfs+ only)
85 # attributes-osx see chflags
91 # type - regular dir ...
92 # times - creation, modification, posix change, access
95 # attributes - see attrib
97 # forks (alternate data streams)
101 # type - regular dir ...
102 # perms - rwxrwxrwx (maybe - see wikipedia)
103 # times - creation, modification, access
104 # attributes - see attrib
108 _have_lchmod = hasattr(os, 'lchmod')
111 def _clean_up_path_for_archive(p):
112 # Not the most efficient approach.
115 # Take everything after any '/../'.
116 pos = result.rfind('/../')
118 result = result[result.rfind('/../') + 4:]
120 # Take everything after any remaining '../'.
121 if result.startswith("../"):
124 # Remove any '/./' sequences.
125 pos = result.find('/./')
127 result = result[0:pos] + '/' + result[pos + 3:]
128 pos = result.find('/./')
130 # Remove any leading '/'s.
131 result = result.lstrip('/')
133 # Replace '//' with '/' everywhere.
134 pos = result.find('//')
136 result = result[0:pos] + '/' + result[pos + 2:]
137 pos = result.find('//')
139 # Take everything after any remaining './'.
140 if result.startswith('./'):
143 # Take everything before any remaining '/.'.
144 if result.endswith('/.'):
147 if result == '' or result.endswith('/..'):
154 if p.startswith('/'):
156 if p.find('/../') != -1:
158 if p.startswith('../'):
160 if p.endswith('/..'):
165 def _clean_up_extract_path(p):
166 result = p.lstrip('/')
169 elif _risky_path(result):
175 # These tags are currently conceptually private to Metadata, and they
176 # must be unique, and must *never* be changed.
179 _rec_tag_common = 2 # times, user, group, type, perms, etc. (legacy/broken)
180 _rec_tag_symlink_target = 3
181 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
182 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
183 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
184 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
185 _rec_tag_hardlink_target = 8 # hard link target path
186 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
189 class ApplyError(Exception):
190 # Thrown when unable to apply any given bit of metadata to a path.
195 # Metadata is stored as a sequence of tagged binary records. Each
196 # record will have some subset of add, encode, load, create, and
197 # apply methods, i.e. _add_foo...
199 # We do allow an "empty" object as a special case, i.e. no
200 # records. One can be created by trying to write Metadata(), and
201 # for such an object, read() will return None. This is used by
202 # "bup save", for example, as a placeholder in cases where
205 # NOTE: if any relevant fields are added or removed, be sure to
206 # update same_file() below.
210 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
211 # must be non-negative and < 10**9.
213 def _add_common(self, path, st):
216 self.atime = st.st_atime
217 self.mtime = st.st_mtime
218 self.ctime = st.st_ctime
219 self.user = self.group = ''
220 entry = pwd_from_uid(st.st_uid)
222 self.user = entry.pw_name
223 entry = grp_from_gid(st.st_gid)
225 self.group = entry.gr_name
226 self.mode = st.st_mode
227 # Only collect st_rdev if we might need it for a mknod()
228 # during restore. On some platforms (i.e. kFreeBSD), it isn't
229 # stable for other file types. For example "cp -a" will
230 # change it for a plain file.
231 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
232 self.rdev = st.st_rdev
236 def _same_common(self, other):
237 """Return true or false to indicate similarity in the hardlink sense."""
238 return self.uid == other.uid \
239 and self.gid == other.gid \
240 and self.rdev == other.rdev \
241 and self.mtime == other.mtime \
242 and self.ctime == other.ctime \
243 and self.user == other.user \
244 and self.group == other.group
246 def _encode_common(self):
249 atime = xstat.nsecs_to_timespec(self.atime)
250 mtime = xstat.nsecs_to_timespec(self.mtime)
251 ctime = xstat.nsecs_to_timespec(self.ctime)
252 result = vint.pack('vvsvsvvVvVvV',
267 def _load_common_rec(self, port, legacy_format=False):
268 unpack_fmt = 'vvsvsvvVvVvV'
270 unpack_fmt = 'VVsVsVvVvVvV'
271 data = vint.read_bvec(port)
283 ctime_ns) = vint.unpack(unpack_fmt, data)
284 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
285 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
286 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
288 def _recognized_file_type(self):
289 return stat.S_ISREG(self.mode) \
290 or stat.S_ISDIR(self.mode) \
291 or stat.S_ISCHR(self.mode) \
292 or stat.S_ISBLK(self.mode) \
293 or stat.S_ISFIFO(self.mode) \
294 or stat.S_ISSOCK(self.mode) \
295 or stat.S_ISLNK(self.mode)
297 def _create_via_common_rec(self, path, create_symlinks=True):
299 raise ApplyError('no metadata - cannot create path ' + path)
301 # If the path already exists and is a dir, try rmdir.
302 # If the path already exists and is anything else, try unlink.
305 st = xstat.lstat(path)
307 if e.errno != errno.ENOENT:
310 if stat.S_ISDIR(st.st_mode):
314 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
315 msg = 'refusing to overwrite non-empty dir ' + path
321 if stat.S_ISREG(self.mode):
322 assert(self._recognized_file_type())
323 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
325 elif stat.S_ISDIR(self.mode):
326 assert(self._recognized_file_type())
328 elif stat.S_ISCHR(self.mode):
329 assert(self._recognized_file_type())
330 os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
331 elif stat.S_ISBLK(self.mode):
332 assert(self._recognized_file_type())
333 os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
334 elif stat.S_ISFIFO(self.mode):
335 assert(self._recognized_file_type())
336 os.mknod(path, 0600 | stat.S_IFIFO)
337 elif stat.S_ISSOCK(self.mode):
339 os.mknod(path, 0600 | stat.S_IFSOCK)
341 if e.errno in (errno.EINVAL, errno.EPERM):
342 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
346 elif stat.S_ISLNK(self.mode):
347 assert(self._recognized_file_type())
348 if self.symlink_target and create_symlinks:
349 # on MacOS, symlink() permissions depend on umask, and there's
350 # no way to chown a symlink after creating it, so we have to
352 oldumask = os.umask((self.mode & 0777) ^ 0777)
354 os.symlink(self.symlink_target, path)
357 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
359 assert(not self._recognized_file_type())
360 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
363 def _apply_common_rec(self, path, restore_numeric_ids=False):
365 raise ApplyError('no metadata - cannot apply to ' + path)
367 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
368 # EACCES errors at this stage are fatal for the current path.
369 if lutime and stat.S_ISLNK(self.mode):
371 lutime(path, (self.atime, self.mtime))
373 if e.errno == errno.EACCES:
374 raise ApplyError('lutime: %s' % e)
379 utime(path, (self.atime, self.mtime))
381 if e.errno == errno.EACCES:
382 raise ApplyError('utime: %s' % e)
386 uid = gid = -1 # By default, do nothing.
390 if not restore_numeric_ids:
391 if self.uid != 0 and self.user:
392 entry = pwd_from_name(self.user)
395 if self.gid != 0 and self.group:
396 entry = grp_from_name(self.group)
399 else: # not superuser - only consider changing the group/gid
400 user_gids = os.getgroups()
401 if self.gid in user_gids:
403 if not restore_numeric_ids and self.gid != 0:
404 # The grp might not exist on the local system.
405 grps = filter(None, [grp_from_gid(x) for x in user_gids])
406 if self.group in [x.gr_name for x in grps]:
407 g = grp_from_name(self.group)
411 if uid != -1 or gid != -1:
413 os.lchown(path, uid, gid)
415 if e.errno == errno.EPERM:
416 add_error('lchown: %s' % e)
417 elif sys.platform.startswith('cygwin') \
418 and e.errno == errno.EINVAL:
419 add_error('lchown: unknown uid/gid (%d/%d) for %s'
425 os.lchmod(path, stat.S_IMODE(self.mode))
426 elif not stat.S_ISLNK(self.mode):
427 os.chmod(path, stat.S_IMODE(self.mode))
432 def _encode_path(self):
434 return vint.pack('s', self.path)
438 def _load_path_rec(self, port):
439 self.path = vint.unpack('s', vint.read_bvec(port))[0]
444 def _add_symlink_target(self, path, st):
446 if stat.S_ISLNK(st.st_mode):
447 self.symlink_target = os.readlink(path)
449 add_error('readlink: %s', e)
451 def _encode_symlink_target(self):
452 return self.symlink_target
454 def _load_symlink_target_rec(self, port):
455 self.symlink_target = vint.read_bvec(port)
460 def _add_hardlink_target(self, target):
461 self.hardlink_target = target
463 def _same_hardlink_target(self, other):
464 """Return true or false to indicate similarity in the hardlink sense."""
465 return self.hardlink_target == other.hardlink_target
467 def _encode_hardlink_target(self):
468 return self.hardlink_target
470 def _load_hardlink_target_rec(self, port):
471 self.hardlink_target = vint.read_bvec(port)
474 ## POSIX1e ACL records
476 # Recorded as a list:
477 # [txt_id_acl, num_id_acl]
478 # or, if a directory:
479 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
480 # The numeric/text distinction only matters when reading/restoring
482 def _add_posix1e_acl(self, path, st):
483 if not posix1e: return
484 if not stat.S_ISLNK(st.st_mode):
488 if posix1e.has_extended(path):
489 acl = posix1e.ACL(file=path)
490 acls = [acl, acl] # txt and num are the same
491 if stat.S_ISDIR(st.st_mode):
492 def_acl = posix1e.ACL(filedef=path)
493 def_acls = [def_acl, def_acl]
494 except EnvironmentError, e:
495 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
498 txt_flags = posix1e.TEXT_ABBREVIATE
499 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
500 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
501 acls[1].to_any_text('', '\n', num_flags)]
503 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
504 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
505 self.posix1e_acl = acl_rep
507 def _same_posix1e_acl(self, other):
508 """Return true or false to indicate similarity in the hardlink sense."""
509 return self.posix1e_acl == other.posix1e_acl
511 def _encode_posix1e_acl(self):
512 # Encode as two strings (w/default ACL string possibly empty).
514 acls = self.posix1e_acl
516 acls.extend(['', ''])
517 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
521 def _load_posix1e_acl_rec(self, port):
522 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
524 acl_rep = acl_rep[:2]
525 self.posix1e_acl = acl_rep
527 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
528 def apply_acl(acl_rep, kind):
530 acl = posix1e.ACL(text = acl_rep)
533 # pylibacl appears to return an IOError with errno
534 # set to 0 if a group referred to by the ACL rep
535 # doesn't exist on the current system.
536 raise ApplyError("POSIX1e ACL: can't create %r for %r"
541 acl.applyto(path, kind)
543 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
544 raise ApplyError('POSIX1e ACL applyto: %s' % e)
550 add_error("%s: can't restore ACLs; posix1e support missing.\n"
554 acls = self.posix1e_acl
556 if restore_numeric_ids:
557 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
559 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
560 if restore_numeric_ids:
561 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
563 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
566 ## Linux attributes (lsattr(1), chattr(1))
568 def _add_linux_attr(self, path, st):
569 check_linux_file_attr_api()
570 if not get_linux_file_attr: return
571 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
573 attr = get_linux_file_attr(path)
575 self.linux_attr = attr
577 if e.errno == errno.EACCES:
578 add_error('read Linux attr: %s' % e)
579 elif e.errno in (errno.ENOTTY, errno.ENOSYS, errno.EOPNOTSUPP):
580 # Assume filesystem doesn't support attrs.
585 def _same_linux_attr(self, other):
586 """Return true or false to indicate similarity in the hardlink sense."""
587 return self.linux_attr == other.linux_attr
589 def _encode_linux_attr(self):
591 return vint.pack('V', self.linux_attr)
595 def _load_linux_attr_rec(self, port):
596 data = vint.read_bvec(port)
597 self.linux_attr = vint.unpack('V', data)[0]
599 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
601 check_linux_file_attr_api()
602 if not set_linux_file_attr:
603 add_error("%s: can't restore linuxattrs: "
604 "linuxattr support missing.\n" % path)
607 set_linux_file_attr(path, self.linux_attr)
609 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS,
611 raise ApplyError('Linux chattr: %s (0x%s)'
612 % (e, hex(self.linux_attr)))
617 ## Linux extended attributes (getfattr(1), setfattr(1))
619 def _add_linux_xattr(self, path, st):
622 self.linux_xattr = xattr.get_all(path, nofollow=True)
623 except EnvironmentError, e:
624 if e.errno != errno.EOPNOTSUPP:
627 def _same_linux_xattr(self, other):
628 """Return true or false to indicate similarity in the hardlink sense."""
629 return self.linux_xattr == other.linux_xattr
631 def _encode_linux_xattr(self):
633 result = vint.pack('V', len(self.linux_xattr))
634 for name, value in self.linux_xattr:
635 result += vint.pack('ss', name, value)
640 def _load_linux_xattr_rec(self, file):
641 data = vint.read_bvec(file)
642 memfile = StringIO(data)
644 for i in range(vint.read_vuint(memfile)):
645 key = vint.read_bvec(memfile)
646 value = vint.read_bvec(memfile)
647 result.append((key, value))
648 self.linux_xattr = result
650 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
653 add_error("%s: can't restore xattr; xattr support missing.\n"
656 if not self.linux_xattr:
659 existing_xattrs = set(xattr.list(path, nofollow=True))
661 if e.errno == errno.EACCES:
662 raise ApplyError('xattr.set %r: %s' % (path, e))
665 for k, v in self.linux_xattr:
666 if k not in existing_xattrs \
667 or v != xattr.get(path, k, nofollow=True):
669 xattr.set(path, k, v, nofollow=True)
671 if e.errno == errno.EPERM \
672 or e.errno == errno.EOPNOTSUPP:
673 raise ApplyError('xattr.set %r: %s' % (path, e))
676 existing_xattrs -= frozenset([k])
677 for k in existing_xattrs:
679 xattr.remove(path, k, nofollow=True)
681 if e.errno == errno.EPERM:
682 raise ApplyError('xattr.remove %r: %s' % (path, e))
687 self.mode = self.uid = self.gid = self.user = self.group = None
688 self.atime = self.mtime = self.ctime = None
692 self.symlink_target = None
693 self.hardlink_target = None
694 self.linux_attr = None
695 self.linux_xattr = None
696 self.posix1e_acl = None
699 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
701 result += ' path:' + repr(self.path)
703 result += ' mode:' + repr(xstat.mode_str(self.mode)
704 + '(%s)' % hex(self.mode))
706 result += ' uid:' + str(self.uid)
708 result += ' gid:' + str(self.gid)
710 result += ' user:' + repr(self.user)
712 result += ' group:' + repr(self.group)
714 result += ' size:' + repr(self.size)
715 for name, val in (('atime', self.atime),
716 ('mtime', self.mtime),
717 ('ctime', self.ctime)):
720 time.strftime('%Y-%m-%d %H:%M %z',
721 time.gmtime(xstat.fstime_floor_secs(val))))
723 return ''.join(result)
725 def write(self, port, include_path=True):
726 records = include_path and [(_rec_tag_path, self._encode_path())] or []
727 records.extend([(_rec_tag_common_v2, self._encode_common()),
728 (_rec_tag_symlink_target,
729 self._encode_symlink_target()),
730 (_rec_tag_hardlink_target,
731 self._encode_hardlink_target()),
732 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
733 (_rec_tag_linux_attr, self._encode_linux_attr()),
734 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
735 for tag, data in records:
737 vint.write_vuint(port, tag)
738 vint.write_bvec(port, data)
739 vint.write_vuint(port, _rec_tag_end)
741 def encode(self, include_path=True):
743 self.write(port, include_path)
744 return port.getvalue()
748 # This method should either return a valid Metadata object,
749 # return None if there was no information at all (just a
750 # _rec_tag_end), throw EOFError if there was nothing at all to
751 # read, or throw an Exception if a valid object could not be
753 tag = vint.read_vuint(port)
754 if tag == _rec_tag_end:
756 try: # From here on, EOF is an error.
758 while True: # only exit is error (exception) or _rec_tag_end
759 if tag == _rec_tag_path:
760 result._load_path_rec(port)
761 elif tag == _rec_tag_common_v2:
762 result._load_common_rec(port)
763 elif tag == _rec_tag_symlink_target:
764 result._load_symlink_target_rec(port)
765 elif tag == _rec_tag_hardlink_target:
766 result._load_hardlink_target_rec(port)
767 elif tag == _rec_tag_posix1e_acl:
768 result._load_posix1e_acl_rec(port)
769 elif tag == _rec_tag_linux_attr:
770 result._load_linux_attr_rec(port)
771 elif tag == _rec_tag_linux_xattr:
772 result._load_linux_xattr_rec(port)
773 elif tag == _rec_tag_end:
775 elif tag == _rec_tag_common: # Should be very rare.
776 result._load_common_rec(port, legacy_format = True)
777 else: # unknown record
779 tag = vint.read_vuint(port)
781 raise Exception("EOF while reading Metadata")
784 return stat.S_ISDIR(self.mode)
786 def create_path(self, path, create_symlinks=True):
787 self._create_via_common_rec(path, create_symlinks=create_symlinks)
789 def apply_to_path(self, path=None, restore_numeric_ids=False):
790 # apply metadata to path -- file must exist
794 raise Exception('Metadata.apply_to_path() called with no path')
795 if not self._recognized_file_type():
796 add_error('not applying metadata to "%s"' % path
797 + ' with unrecognized mode "0x%x"\n' % self.mode)
799 num_ids = restore_numeric_ids
800 for apply_metadata in (self._apply_common_rec,
801 self._apply_posix1e_acl_rec,
802 self._apply_linux_attr_rec,
803 self._apply_linux_xattr_rec):
805 apply_metadata(path, restore_numeric_ids=num_ids)
806 except ApplyError, e:
809 def same_file(self, other):
810 """Compare this to other for equivalency. Return true if
811 their information implies they could represent the same file
812 on disk, in the hardlink sense. Assume they're both regular
814 return self._same_common(other) \
815 and self._same_hardlink_target(other) \
816 and self._same_posix1e_acl(other) \
817 and self._same_linux_attr(other) \
818 and self._same_linux_xattr(other)
821 def from_path(path, statinfo=None, archive_path=None,
822 save_symlinks=True, hardlink_target=None):
824 result.path = archive_path
825 st = statinfo or xstat.lstat(path)
826 result.size = st.st_size
827 result._add_common(path, st)
829 result._add_symlink_target(path, st)
830 result._add_hardlink_target(hardlink_target)
831 result._add_posix1e_acl(path, st)
832 result._add_linux_attr(path, st)
833 result._add_linux_xattr(path, st)
837 def save_tree(output_file, paths,
843 # Issue top-level rewrite warnings.
845 safe_path = _clean_up_path_for_archive(path)
846 if safe_path != path:
847 log('archiving "%s" as "%s"\n' % (path, safe_path))
851 safe_path = _clean_up_path_for_archive(p)
853 if stat.S_ISDIR(st.st_mode):
855 m = from_path(p, statinfo=st, archive_path=safe_path,
856 save_symlinks=save_symlinks)
858 print >> sys.stderr, m.path
859 m.write(output_file, include_path=write_paths)
861 start_dir = os.getcwd()
863 for (p, st) in recursive_dirlist(paths, xdev=xdev):
864 dirlist_dir = os.getcwd()
866 safe_path = _clean_up_path_for_archive(p)
867 m = from_path(p, statinfo=st, archive_path=safe_path,
868 save_symlinks=save_symlinks)
870 print >> sys.stderr, m.path
871 m.write(output_file, include_path=write_paths)
872 os.chdir(dirlist_dir)
877 def _set_up_path(meta, create_symlinks=True):
878 # Allow directories to exist as a special case -- might have
879 # been created by an earlier longer path.
883 parent = os.path.dirname(meta.path)
886 meta.create_path(meta.path, create_symlinks=create_symlinks)
889 all_fields = frozenset(['path',
906 def summary_str(meta, numeric_ids = False, classification = None,
907 human_readable = False):
909 """Return a string containing the "ls -l" style listing for meta.
910 Classification may be "all", "type", or None."""
911 user_str = group_str = size_or_dev_str = '?'
912 symlink_target = None
915 mode_str = xstat.mode_str(meta.mode)
916 symlink_target = meta.symlink_target
917 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
918 mtime_str = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
919 if meta.user and not numeric_ids:
921 elif meta.uid != None:
922 user_str = str(meta.uid)
923 if meta.group and not numeric_ids:
924 group_str = meta.group
925 elif meta.gid != None:
926 group_str = str(meta.gid)
927 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
929 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
931 elif meta.size != None:
933 size_or_dev_str = format_filesize(meta.size)
935 size_or_dev_str = str(meta.size)
937 size_or_dev_str = '-'
939 classification_str = \
940 xstat.classification_str(meta.mode, classification == 'all')
943 mtime_str = '????-??-?? ??:??'
944 classification_str = '?'
948 name += classification_str
950 name += ' -> ' + meta.symlink_target
952 return '%-10s %-11s %11s %16s %s' % (mode_str,
953 user_str + "/" + group_str,
959 def detailed_str(meta, fields = None):
960 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
961 # 0", "link-target:", etc.
967 path = meta.path or ''
968 result.append('path: ' + path)
970 result.append('mode: %s (%s)' % (oct(meta.mode),
971 xstat.mode_str(meta.mode)))
972 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
973 result.append('link-target: ' + meta.symlink_target)
976 result.append('rdev: %d,%d' % (os.major(meta.rdev),
977 os.minor(meta.rdev)))
979 result.append('rdev: 0')
980 if 'size' in fields and meta.size:
981 result.append('size: ' + str(meta.size))
983 result.append('uid: ' + str(meta.uid))
985 result.append('gid: ' + str(meta.gid))
987 result.append('user: ' + meta.user)
988 if 'group' in fields:
989 result.append('group: ' + meta.group)
990 if 'atime' in fields:
991 # If we don't have xstat.lutime, that means we have to use
992 # utime(), and utime() has no way to set the mtime/atime of a
993 # symlink. Thus, the mtime/atime of a symlink is meaningless,
994 # so let's not report it. (That way scripts comparing
995 # before/after won't trigger.)
996 if xstat.lutime or not stat.S_ISLNK(meta.mode):
997 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
999 result.append('atime: 0')
1000 if 'mtime' in fields:
1001 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1002 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1004 result.append('mtime: 0')
1005 if 'ctime' in fields:
1006 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1007 if 'linux-attr' in fields and meta.linux_attr:
1008 result.append('linux-attr: ' + hex(meta.linux_attr))
1009 if 'linux-xattr' in fields and meta.linux_xattr:
1010 for name, value in meta.linux_xattr:
1011 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1012 if 'posix1e-acl' in fields and meta.posix1e_acl:
1013 acl = meta.posix1e_acl[0]
1014 result.append('posix1e-acl: ' + acl + '\n')
1015 if stat.S_ISDIR(meta.mode):
1016 def_acl = meta.posix1e_acl[2]
1017 result.append('posix1e-acl-default: ' + def_acl + '\n')
1018 return '\n'.join(result)
1021 class _ArchiveIterator:
1024 return Metadata.read(self._file)
1026 raise StopIteration()
1031 def __init__(self, file):
1035 def display_archive(file):
1038 for meta in _ArchiveIterator(file):
1041 print detailed_str(meta)
1044 for meta in _ArchiveIterator(file):
1045 print summary_str(meta)
1047 for meta in _ArchiveIterator(file):
1049 print >> sys.stderr, \
1050 'bup: no metadata path, but asked to only display path', \
1051 '(increase verbosity?)'
1056 def start_extract(file, create_symlinks=True):
1057 for meta in _ArchiveIterator(file):
1058 if not meta: # Hit end record.
1061 print >> sys.stderr, meta.path
1062 xpath = _clean_up_extract_path(meta.path)
1064 add_error(Exception('skipping risky path "%s"' % meta.path))
1067 _set_up_path(meta, create_symlinks=create_symlinks)
1070 def finish_extract(file, restore_numeric_ids=False):
1072 for meta in _ArchiveIterator(file):
1073 if not meta: # Hit end record.
1075 xpath = _clean_up_extract_path(meta.path)
1077 add_error(Exception('skipping risky path "%s"' % dir.path))
1079 if os.path.isdir(meta.path):
1080 all_dirs.append(meta)
1083 print >> sys.stderr, meta.path
1084 meta.apply_to_path(path=xpath,
1085 restore_numeric_ids=restore_numeric_ids)
1086 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1087 for dir in all_dirs:
1088 # Don't need to check xpath -- won't be in all_dirs if not OK.
1089 xpath = _clean_up_extract_path(dir.path)
1091 print >> sys.stderr, dir.path
1092 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1095 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1096 # For now, just store all the directories and handle them last,
1099 for meta in _ArchiveIterator(file):
1100 if not meta: # Hit end record.
1102 xpath = _clean_up_extract_path(meta.path)
1104 add_error(Exception('skipping risky path "%s"' % meta.path))
1108 print >> sys.stderr, '+', meta.path
1109 _set_up_path(meta, create_symlinks=create_symlinks)
1110 if os.path.isdir(meta.path):
1111 all_dirs.append(meta)
1114 print >> sys.stderr, '=', meta.path
1115 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1116 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1117 for dir in all_dirs:
1118 # Don't need to check xpath -- won't be in all_dirs if not OK.
1119 xpath = _clean_up_extract_path(dir.path)
1121 print >> sys.stderr, '=', xpath
1122 # Shouldn't have to check for risky paths here (omitted above).
1123 dir.apply_to_path(path=dir.path,
1124 restore_numeric_ids=restore_numeric_ids)