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):
217 self.atime = st.st_atime
218 self.mtime = st.st_mtime
219 self.ctime = st.st_ctime
220 self.user = self.group = ''
221 entry = pwd_from_uid(st.st_uid)
223 self.user = entry.pw_name
224 entry = grp_from_gid(st.st_gid)
226 self.group = entry.gr_name
227 self.mode = st.st_mode
228 # Only collect st_rdev if we might need it for a mknod()
229 # during restore. On some platforms (i.e. kFreeBSD), it isn't
230 # stable for other file types. For example "cp -a" will
231 # change it for a plain file.
232 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
233 self.rdev = st.st_rdev
237 def _same_common(self, other):
238 """Return true or false to indicate similarity in the hardlink sense."""
239 return self.uid == other.uid \
240 and self.gid == other.gid \
241 and self.rdev == other.rdev \
242 and self.mtime == other.mtime \
243 and self.ctime == other.ctime \
244 and self.user == other.user \
245 and self.group == other.group
247 def _encode_common(self):
250 atime = xstat.nsecs_to_timespec(self.atime)
251 mtime = xstat.nsecs_to_timespec(self.mtime)
252 ctime = xstat.nsecs_to_timespec(self.ctime)
253 result = vint.pack('vvsvsvvVvVvV',
268 def _load_common_rec(self, port, legacy_format=False):
269 unpack_fmt = 'vvsvsvvVvVvV'
271 unpack_fmt = 'VVsVsVvVvVvV'
272 data = vint.read_bvec(port)
284 ctime_ns) = vint.unpack(unpack_fmt, data)
285 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
286 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
287 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
289 def _recognized_file_type(self):
290 return stat.S_ISREG(self.mode) \
291 or stat.S_ISDIR(self.mode) \
292 or stat.S_ISCHR(self.mode) \
293 or stat.S_ISBLK(self.mode) \
294 or stat.S_ISFIFO(self.mode) \
295 or stat.S_ISSOCK(self.mode) \
296 or stat.S_ISLNK(self.mode)
298 def _create_via_common_rec(self, path, create_symlinks=True):
300 raise ApplyError('no metadata - cannot create path ' + path)
302 # If the path already exists and is a dir, try rmdir.
303 # If the path already exists and is anything else, try unlink.
306 st = xstat.lstat(path)
308 if e.errno != errno.ENOENT:
311 if stat.S_ISDIR(st.st_mode):
315 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
316 msg = 'refusing to overwrite non-empty dir ' + path
322 if stat.S_ISREG(self.mode):
323 assert(self._recognized_file_type())
324 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
326 elif stat.S_ISDIR(self.mode):
327 assert(self._recognized_file_type())
329 elif stat.S_ISCHR(self.mode):
330 assert(self._recognized_file_type())
331 os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
332 elif stat.S_ISBLK(self.mode):
333 assert(self._recognized_file_type())
334 os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
335 elif stat.S_ISFIFO(self.mode):
336 assert(self._recognized_file_type())
337 os.mknod(path, 0600 | stat.S_IFIFO)
338 elif stat.S_ISSOCK(self.mode):
340 os.mknod(path, 0600 | stat.S_IFSOCK)
342 if e.errno in (errno.EINVAL, errno.EPERM):
343 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
347 elif stat.S_ISLNK(self.mode):
348 assert(self._recognized_file_type())
349 if self.symlink_target and create_symlinks:
350 # on MacOS, symlink() permissions depend on umask, and there's
351 # no way to chown a symlink after creating it, so we have to
353 oldumask = os.umask((self.mode & 0777) ^ 0777)
355 os.symlink(self.symlink_target, path)
358 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
360 assert(not self._recognized_file_type())
361 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
364 def _apply_common_rec(self, path, restore_numeric_ids=False):
366 raise ApplyError('no metadata - cannot apply to ' + path)
368 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
369 # EACCES errors at this stage are fatal for the current path.
370 if lutime and stat.S_ISLNK(self.mode):
372 lutime(path, (self.atime, self.mtime))
374 if e.errno == errno.EACCES:
375 raise ApplyError('lutime: %s' % e)
380 utime(path, (self.atime, self.mtime))
382 if e.errno == errno.EACCES:
383 raise ApplyError('utime: %s' % e)
387 uid = gid = -1 # By default, do nothing.
391 if not restore_numeric_ids:
392 if self.uid != 0 and self.user:
393 entry = pwd_from_name(self.user)
396 if self.gid != 0 and self.group:
397 entry = grp_from_name(self.group)
400 else: # not superuser - only consider changing the group/gid
401 user_gids = os.getgroups()
402 if self.gid in user_gids:
404 if not restore_numeric_ids and self.gid != 0:
405 # The grp might not exist on the local system.
406 grps = filter(None, [grp_from_gid(x) for x in user_gids])
407 if self.group in [x.gr_name for x in grps]:
408 g = grp_from_name(self.group)
412 if uid != -1 or gid != -1:
414 os.lchown(path, uid, gid)
416 if e.errno == errno.EPERM:
417 add_error('lchown: %s' % e)
418 elif sys.platform.startswith('cygwin') \
419 and e.errno == errno.EINVAL:
420 add_error('lchown: unknown uid/gid (%d/%d) for %s'
426 os.lchmod(path, stat.S_IMODE(self.mode))
427 elif not stat.S_ISLNK(self.mode):
428 os.chmod(path, stat.S_IMODE(self.mode))
433 def _encode_path(self):
435 return vint.pack('s', self.path)
439 def _load_path_rec(self, port):
440 self.path = vint.unpack('s', vint.read_bvec(port))[0]
445 def _add_symlink_target(self, path, st):
447 if stat.S_ISLNK(st.st_mode):
448 self.symlink_target = os.readlink(path)
450 add_error('readlink: %s' % e)
452 def _encode_symlink_target(self):
453 return self.symlink_target
455 def _load_symlink_target_rec(self, port):
456 self.symlink_target = vint.read_bvec(port)
461 def _add_hardlink_target(self, target):
462 self.hardlink_target = target
464 def _same_hardlink_target(self, other):
465 """Return true or false to indicate similarity in the hardlink sense."""
466 return self.hardlink_target == other.hardlink_target
468 def _encode_hardlink_target(self):
469 return self.hardlink_target
471 def _load_hardlink_target_rec(self, port):
472 self.hardlink_target = vint.read_bvec(port)
475 ## POSIX1e ACL records
477 # Recorded as a list:
478 # [txt_id_acl, num_id_acl]
479 # or, if a directory:
480 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
481 # The numeric/text distinction only matters when reading/restoring
483 def _add_posix1e_acl(self, path, st):
484 if not posix1e: return
485 if not stat.S_ISLNK(st.st_mode):
489 if posix1e.has_extended(path):
490 acl = posix1e.ACL(file=path)
491 acls = [acl, acl] # txt and num are the same
492 if stat.S_ISDIR(st.st_mode):
493 def_acl = posix1e.ACL(filedef=path)
494 def_acls = [def_acl, def_acl]
495 except EnvironmentError, e:
496 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
499 txt_flags = posix1e.TEXT_ABBREVIATE
500 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
501 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
502 acls[1].to_any_text('', '\n', num_flags)]
504 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
505 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
506 self.posix1e_acl = acl_rep
508 def _same_posix1e_acl(self, other):
509 """Return true or false to indicate similarity in the hardlink sense."""
510 return self.posix1e_acl == other.posix1e_acl
512 def _encode_posix1e_acl(self):
513 # Encode as two strings (w/default ACL string possibly empty).
515 acls = self.posix1e_acl
517 acls.extend(['', ''])
518 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
522 def _load_posix1e_acl_rec(self, port):
523 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
525 acl_rep = acl_rep[:2]
526 self.posix1e_acl = acl_rep
528 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
529 def apply_acl(acl_rep, kind):
531 acl = posix1e.ACL(text = acl_rep)
534 # pylibacl appears to return an IOError with errno
535 # set to 0 if a group referred to by the ACL rep
536 # doesn't exist on the current system.
537 raise ApplyError("POSIX1e ACL: can't create %r for %r"
542 acl.applyto(path, kind)
544 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
545 raise ApplyError('POSIX1e ACL applyto: %s' % e)
551 add_error("%s: can't restore ACLs; posix1e support missing.\n"
555 acls = self.posix1e_acl
557 if restore_numeric_ids:
558 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
560 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
561 if restore_numeric_ids:
562 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
564 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
567 ## Linux attributes (lsattr(1), chattr(1))
569 def _add_linux_attr(self, path, st):
570 check_linux_file_attr_api()
571 if not get_linux_file_attr: return
572 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
574 attr = get_linux_file_attr(path)
576 self.linux_attr = attr
578 if e.errno == errno.EACCES:
579 add_error('read Linux attr: %s' % e)
580 elif e.errno in (errno.ENOTTY, errno.ENOSYS, errno.EOPNOTSUPP):
581 # Assume filesystem doesn't support attrs.
586 def _same_linux_attr(self, other):
587 """Return true or false to indicate similarity in the hardlink sense."""
588 return self.linux_attr == other.linux_attr
590 def _encode_linux_attr(self):
592 return vint.pack('V', self.linux_attr)
596 def _load_linux_attr_rec(self, port):
597 data = vint.read_bvec(port)
598 self.linux_attr = vint.unpack('V', data)[0]
600 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
602 check_linux_file_attr_api()
603 if not set_linux_file_attr:
604 add_error("%s: can't restore linuxattrs: "
605 "linuxattr support missing.\n" % path)
608 set_linux_file_attr(path, self.linux_attr)
610 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS,
612 raise ApplyError('Linux chattr: %s (0x%s)'
613 % (e, hex(self.linux_attr)))
618 ## Linux extended attributes (getfattr(1), setfattr(1))
620 def _add_linux_xattr(self, path, st):
623 self.linux_xattr = xattr.get_all(path, nofollow=True)
624 except EnvironmentError, e:
625 if e.errno != errno.EOPNOTSUPP:
628 def _same_linux_xattr(self, other):
629 """Return true or false to indicate similarity in the hardlink sense."""
630 return self.linux_xattr == other.linux_xattr
632 def _encode_linux_xattr(self):
634 result = vint.pack('V', len(self.linux_xattr))
635 for name, value in self.linux_xattr:
636 result += vint.pack('ss', name, value)
641 def _load_linux_xattr_rec(self, file):
642 data = vint.read_bvec(file)
643 memfile = StringIO(data)
645 for i in range(vint.read_vuint(memfile)):
646 key = vint.read_bvec(memfile)
647 value = vint.read_bvec(memfile)
648 result.append((key, value))
649 self.linux_xattr = result
651 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
654 add_error("%s: can't restore xattr; xattr support missing.\n"
657 if not self.linux_xattr:
660 existing_xattrs = set(xattr.list(path, nofollow=True))
662 if e.errno == errno.EACCES:
663 raise ApplyError('xattr.set %r: %s' % (path, e))
666 for k, v in self.linux_xattr:
667 if k not in existing_xattrs \
668 or v != xattr.get(path, k, nofollow=True):
670 xattr.set(path, k, v, nofollow=True)
672 if e.errno == errno.EPERM \
673 or e.errno == errno.EOPNOTSUPP:
674 raise ApplyError('xattr.set %r: %s' % (path, e))
677 existing_xattrs -= frozenset([k])
678 for k in existing_xattrs:
680 xattr.remove(path, k, nofollow=True)
682 if e.errno == errno.EPERM:
683 raise ApplyError('xattr.remove %r: %s' % (path, e))
688 self.mode = self.uid = self.gid = self.user = self.group = None
689 self.atime = self.mtime = self.ctime = None
693 self.symlink_target = None
694 self.hardlink_target = None
695 self.linux_attr = None
696 self.linux_xattr = None
697 self.posix1e_acl = None
700 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
702 result += ' path:' + repr(self.path)
704 result += ' mode:' + repr(xstat.mode_str(self.mode)
705 + '(%s)' % hex(self.mode))
707 result += ' uid:' + str(self.uid)
709 result += ' gid:' + str(self.gid)
711 result += ' user:' + repr(self.user)
713 result += ' group:' + repr(self.group)
715 result += ' size:' + repr(self.size)
716 for name, val in (('atime', self.atime),
717 ('mtime', self.mtime),
718 ('ctime', self.ctime)):
721 time.strftime('%Y-%m-%d %H:%M %z',
722 time.gmtime(xstat.fstime_floor_secs(val))))
724 return ''.join(result)
726 def write(self, port, include_path=True):
727 records = include_path and [(_rec_tag_path, self._encode_path())] or []
728 records.extend([(_rec_tag_common_v2, self._encode_common()),
729 (_rec_tag_symlink_target,
730 self._encode_symlink_target()),
731 (_rec_tag_hardlink_target,
732 self._encode_hardlink_target()),
733 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
734 (_rec_tag_linux_attr, self._encode_linux_attr()),
735 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
736 for tag, data in records:
738 vint.write_vuint(port, tag)
739 vint.write_bvec(port, data)
740 vint.write_vuint(port, _rec_tag_end)
742 def encode(self, include_path=True):
744 self.write(port, include_path)
745 return port.getvalue()
749 # This method should either return a valid Metadata object,
750 # return None if there was no information at all (just a
751 # _rec_tag_end), throw EOFError if there was nothing at all to
752 # read, or throw an Exception if a valid object could not be
754 tag = vint.read_vuint(port)
755 if tag == _rec_tag_end:
757 try: # From here on, EOF is an error.
759 while True: # only exit is error (exception) or _rec_tag_end
760 if tag == _rec_tag_path:
761 result._load_path_rec(port)
762 elif tag == _rec_tag_common_v2:
763 result._load_common_rec(port)
764 elif tag == _rec_tag_symlink_target:
765 result._load_symlink_target_rec(port)
766 elif tag == _rec_tag_hardlink_target:
767 result._load_hardlink_target_rec(port)
768 elif tag == _rec_tag_posix1e_acl:
769 result._load_posix1e_acl_rec(port)
770 elif tag == _rec_tag_linux_attr:
771 result._load_linux_attr_rec(port)
772 elif tag == _rec_tag_linux_xattr:
773 result._load_linux_xattr_rec(port)
774 elif tag == _rec_tag_end:
776 elif tag == _rec_tag_common: # Should be very rare.
777 result._load_common_rec(port, legacy_format = True)
778 else: # unknown record
780 tag = vint.read_vuint(port)
782 raise Exception("EOF while reading Metadata")
785 return stat.S_ISDIR(self.mode)
787 def create_path(self, path, create_symlinks=True):
788 self._create_via_common_rec(path, create_symlinks=create_symlinks)
790 def apply_to_path(self, path=None, restore_numeric_ids=False):
791 # apply metadata to path -- file must exist
795 raise Exception('Metadata.apply_to_path() called with no path')
796 if not self._recognized_file_type():
797 add_error('not applying metadata to "%s"' % path
798 + ' with unrecognized mode "0x%x"\n' % self.mode)
800 num_ids = restore_numeric_ids
801 for apply_metadata in (self._apply_common_rec,
802 self._apply_posix1e_acl_rec,
803 self._apply_linux_attr_rec,
804 self._apply_linux_xattr_rec):
806 apply_metadata(path, restore_numeric_ids=num_ids)
807 except ApplyError, e:
810 def same_file(self, other):
811 """Compare this to other for equivalency. Return true if
812 their information implies they could represent the same file
813 on disk, in the hardlink sense. Assume they're both regular
815 return self._same_common(other) \
816 and self._same_hardlink_target(other) \
817 and self._same_posix1e_acl(other) \
818 and self._same_linux_attr(other) \
819 and self._same_linux_xattr(other)
822 def from_path(path, statinfo=None, archive_path=None,
823 save_symlinks=True, hardlink_target=None):
825 result.path = archive_path
826 st = statinfo or xstat.lstat(path)
827 result.size = st.st_size
828 result._add_common(path, st)
830 result._add_symlink_target(path, st)
831 result._add_hardlink_target(hardlink_target)
832 result._add_posix1e_acl(path, st)
833 result._add_linux_attr(path, st)
834 result._add_linux_xattr(path, st)
838 def save_tree(output_file, paths,
844 # Issue top-level rewrite warnings.
846 safe_path = _clean_up_path_for_archive(path)
847 if safe_path != path:
848 log('archiving "%s" as "%s"\n' % (path, safe_path))
852 safe_path = _clean_up_path_for_archive(p)
854 if stat.S_ISDIR(st.st_mode):
856 m = from_path(p, statinfo=st, archive_path=safe_path,
857 save_symlinks=save_symlinks)
859 print >> sys.stderr, m.path
860 m.write(output_file, include_path=write_paths)
862 start_dir = os.getcwd()
864 for (p, st) in recursive_dirlist(paths, xdev=xdev):
865 dirlist_dir = os.getcwd()
867 safe_path = _clean_up_path_for_archive(p)
868 m = from_path(p, statinfo=st, archive_path=safe_path,
869 save_symlinks=save_symlinks)
871 print >> sys.stderr, m.path
872 m.write(output_file, include_path=write_paths)
873 os.chdir(dirlist_dir)
878 def _set_up_path(meta, create_symlinks=True):
879 # Allow directories to exist as a special case -- might have
880 # been created by an earlier longer path.
884 parent = os.path.dirname(meta.path)
887 meta.create_path(meta.path, create_symlinks=create_symlinks)
890 all_fields = frozenset(['path',
907 def summary_str(meta, numeric_ids = False, classification = None,
908 human_readable = False):
910 """Return a string containing the "ls -l" style listing for meta.
911 Classification may be "all", "type", or None."""
912 user_str = group_str = size_or_dev_str = '?'
913 symlink_target = None
916 mode_str = xstat.mode_str(meta.mode)
917 symlink_target = meta.symlink_target
918 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
919 mtime_str = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
920 if meta.user and not numeric_ids:
922 elif meta.uid != None:
923 user_str = str(meta.uid)
924 if meta.group and not numeric_ids:
925 group_str = meta.group
926 elif meta.gid != None:
927 group_str = str(meta.gid)
928 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
930 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
932 elif meta.size != None:
934 size_or_dev_str = format_filesize(meta.size)
936 size_or_dev_str = str(meta.size)
938 size_or_dev_str = '-'
940 classification_str = \
941 xstat.classification_str(meta.mode, classification == 'all')
944 mtime_str = '????-??-?? ??:??'
945 classification_str = '?'
949 name += classification_str
951 name += ' -> ' + meta.symlink_target
953 return '%-10s %-11s %11s %16s %s' % (mode_str,
954 user_str + "/" + group_str,
960 def detailed_str(meta, fields = None):
961 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
962 # 0", "link-target:", etc.
968 path = meta.path or ''
969 result.append('path: ' + path)
971 result.append('mode: %s (%s)' % (oct(meta.mode),
972 xstat.mode_str(meta.mode)))
973 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
974 result.append('link-target: ' + meta.symlink_target)
977 result.append('rdev: %d,%d' % (os.major(meta.rdev),
978 os.minor(meta.rdev)))
980 result.append('rdev: 0')
981 if 'size' in fields and meta.size:
982 result.append('size: ' + str(meta.size))
984 result.append('uid: ' + str(meta.uid))
986 result.append('gid: ' + str(meta.gid))
988 result.append('user: ' + meta.user)
989 if 'group' in fields:
990 result.append('group: ' + meta.group)
991 if 'atime' in fields:
992 # If we don't have xstat.lutime, that means we have to use
993 # utime(), and utime() has no way to set the mtime/atime of a
994 # symlink. Thus, the mtime/atime of a symlink is meaningless,
995 # so let's not report it. (That way scripts comparing
996 # before/after won't trigger.)
997 if xstat.lutime or not stat.S_ISLNK(meta.mode):
998 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1000 result.append('atime: 0')
1001 if 'mtime' in fields:
1002 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1003 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1005 result.append('mtime: 0')
1006 if 'ctime' in fields:
1007 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1008 if 'linux-attr' in fields and meta.linux_attr:
1009 result.append('linux-attr: ' + hex(meta.linux_attr))
1010 if 'linux-xattr' in fields and meta.linux_xattr:
1011 for name, value in meta.linux_xattr:
1012 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1013 if 'posix1e-acl' in fields and meta.posix1e_acl:
1014 acl = meta.posix1e_acl[0]
1015 result.append('posix1e-acl: ' + acl + '\n')
1016 if stat.S_ISDIR(meta.mode):
1017 def_acl = meta.posix1e_acl[2]
1018 result.append('posix1e-acl-default: ' + def_acl + '\n')
1019 return '\n'.join(result)
1022 class _ArchiveIterator:
1025 return Metadata.read(self._file)
1027 raise StopIteration()
1032 def __init__(self, file):
1036 def display_archive(file):
1039 for meta in _ArchiveIterator(file):
1042 print detailed_str(meta)
1045 for meta in _ArchiveIterator(file):
1046 print summary_str(meta)
1048 for meta in _ArchiveIterator(file):
1050 print >> sys.stderr, \
1051 'bup: no metadata path, but asked to only display path', \
1052 '(increase verbosity?)'
1057 def start_extract(file, create_symlinks=True):
1058 for meta in _ArchiveIterator(file):
1059 if not meta: # Hit end record.
1062 print >> sys.stderr, meta.path
1063 xpath = _clean_up_extract_path(meta.path)
1065 add_error(Exception('skipping risky path "%s"' % meta.path))
1068 _set_up_path(meta, create_symlinks=create_symlinks)
1071 def finish_extract(file, restore_numeric_ids=False):
1073 for meta in _ArchiveIterator(file):
1074 if not meta: # Hit end record.
1076 xpath = _clean_up_extract_path(meta.path)
1078 add_error(Exception('skipping risky path "%s"' % dir.path))
1080 if os.path.isdir(meta.path):
1081 all_dirs.append(meta)
1084 print >> sys.stderr, meta.path
1085 meta.apply_to_path(path=xpath,
1086 restore_numeric_ids=restore_numeric_ids)
1087 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1088 for dir in all_dirs:
1089 # Don't need to check xpath -- won't be in all_dirs if not OK.
1090 xpath = _clean_up_extract_path(dir.path)
1092 print >> sys.stderr, dir.path
1093 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1096 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1097 # For now, just store all the directories and handle them last,
1100 for meta in _ArchiveIterator(file):
1101 if not meta: # Hit end record.
1103 xpath = _clean_up_extract_path(meta.path)
1105 add_error(Exception('skipping risky path "%s"' % meta.path))
1109 print >> sys.stderr, '+', meta.path
1110 _set_up_path(meta, create_symlinks=create_symlinks)
1111 if os.path.isdir(meta.path):
1112 all_dirs.append(meta)
1115 print >> sys.stderr, '=', meta.path
1116 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1117 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1118 for dir in all_dirs:
1119 # Don't need to check xpath -- won't be in all_dirs if not OK.
1120 xpath = _clean_up_extract_path(dir.path)
1122 print >> sys.stderr, '=', xpath
1123 # Shouldn't have to check for risky paths here (omitted above).
1124 dir.apply_to_path(path=dir.path,
1125 restore_numeric_ids=restore_numeric_ids)