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: return
490 if not stat.S_ISLNK(st.st_mode):
494 if posix1e.has_extended(path):
495 acl = posix1e.ACL(file=path)
496 acls = [acl, acl] # txt and num are the same
497 if stat.S_ISDIR(st.st_mode):
498 def_acl = posix1e.ACL(filedef=path)
499 def_acls = [def_acl, def_acl]
500 except EnvironmentError, e:
501 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
504 txt_flags = posix1e.TEXT_ABBREVIATE
505 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
506 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
507 acls[1].to_any_text('', '\n', num_flags)]
509 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
510 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
511 self.posix1e_acl = acl_rep
513 def _same_posix1e_acl(self, other):
514 """Return true or false to indicate similarity in the hardlink sense."""
515 return self.posix1e_acl == other.posix1e_acl
517 def _encode_posix1e_acl(self):
518 # Encode as two strings (w/default ACL string possibly empty).
520 acls = self.posix1e_acl
522 acls.extend(['', ''])
523 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
527 def _load_posix1e_acl_rec(self, port):
528 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
530 acl_rep = acl_rep[:2]
531 self.posix1e_acl = acl_rep
533 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
534 def apply_acl(acl_rep, kind):
536 acl = posix1e.ACL(text = acl_rep)
539 # pylibacl appears to return an IOError with errno
540 # set to 0 if a group referred to by the ACL rep
541 # doesn't exist on the current system.
542 raise ApplyError("POSIX1e ACL: can't create %r for %r"
547 acl.applyto(path, kind)
549 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
550 raise ApplyError('POSIX1e ACL applyto: %s' % e)
556 add_error("%s: can't restore ACLs; posix1e support missing.\n"
560 acls = self.posix1e_acl
562 if restore_numeric_ids:
563 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
565 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
566 if restore_numeric_ids:
567 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
569 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
572 ## Linux attributes (lsattr(1), chattr(1))
574 def _add_linux_attr(self, path, st):
575 check_linux_file_attr_api()
576 if not get_linux_file_attr: return
577 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
579 attr = get_linux_file_attr(path)
581 self.linux_attr = attr
583 if e.errno == errno.EACCES:
584 add_error('read Linux attr: %s' % e)
585 elif e.errno in (errno.ENOTTY, errno.ENOSYS, errno.EOPNOTSUPP):
586 # Assume filesystem doesn't support attrs.
591 def _same_linux_attr(self, other):
592 """Return true or false to indicate similarity in the hardlink sense."""
593 return self.linux_attr == other.linux_attr
595 def _encode_linux_attr(self):
597 return vint.pack('V', self.linux_attr)
601 def _load_linux_attr_rec(self, port):
602 data = vint.read_bvec(port)
603 self.linux_attr = vint.unpack('V', data)[0]
605 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
607 check_linux_file_attr_api()
608 if not set_linux_file_attr:
609 add_error("%s: can't restore linuxattrs: "
610 "linuxattr support missing.\n" % path)
613 set_linux_file_attr(path, self.linux_attr)
615 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS,
617 raise ApplyError('Linux chattr: %s (0x%s)'
618 % (e, hex(self.linux_attr)))
623 ## Linux extended attributes (getfattr(1), setfattr(1))
625 def _add_linux_xattr(self, path, st):
628 self.linux_xattr = xattr.get_all(path, nofollow=True)
629 except EnvironmentError, e:
630 if e.errno != errno.EOPNOTSUPP:
633 def _same_linux_xattr(self, other):
634 """Return true or false to indicate similarity in the hardlink sense."""
635 return self.linux_xattr == other.linux_xattr
637 def _encode_linux_xattr(self):
639 result = vint.pack('V', len(self.linux_xattr))
640 for name, value in self.linux_xattr:
641 result += vint.pack('ss', name, value)
646 def _load_linux_xattr_rec(self, file):
647 data = vint.read_bvec(file)
648 memfile = StringIO(data)
650 for i in range(vint.read_vuint(memfile)):
651 key = vint.read_bvec(memfile)
652 value = vint.read_bvec(memfile)
653 result.append((key, value))
654 self.linux_xattr = result
656 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
659 add_error("%s: can't restore xattr; xattr support missing.\n"
662 if not self.linux_xattr:
665 existing_xattrs = set(xattr.list(path, nofollow=True))
667 if e.errno == errno.EACCES:
668 raise ApplyError('xattr.set %r: %s' % (path, e))
671 for k, v in self.linux_xattr:
672 if k not in existing_xattrs \
673 or v != xattr.get(path, k, nofollow=True):
675 xattr.set(path, k, v, nofollow=True)
677 if e.errno == errno.EPERM \
678 or e.errno == errno.EOPNOTSUPP:
679 raise ApplyError('xattr.set %r: %s' % (path, e))
682 existing_xattrs -= frozenset([k])
683 for k in existing_xattrs:
685 xattr.remove(path, k, nofollow=True)
687 if e.errno == errno.EPERM:
688 raise ApplyError('xattr.remove %r: %s' % (path, e))
693 self.mode = self.uid = self.gid = self.user = self.group = None
694 self.atime = self.mtime = self.ctime = None
698 self.symlink_target = None
699 self.hardlink_target = None
700 self.linux_attr = None
701 self.linux_xattr = None
702 self.posix1e_acl = None
705 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
707 result += ' path:' + repr(self.path)
709 result += ' mode:' + repr(xstat.mode_str(self.mode)
710 + '(%s)' % hex(self.mode))
712 result += ' uid:' + str(self.uid)
714 result += ' gid:' + str(self.gid)
716 result += ' user:' + repr(self.user)
718 result += ' group:' + repr(self.group)
720 result += ' size:' + repr(self.size)
721 for name, val in (('atime', self.atime),
722 ('mtime', self.mtime),
723 ('ctime', self.ctime)):
726 time.strftime('%Y-%m-%d %H:%M %z',
727 time.gmtime(xstat.fstime_floor_secs(val))))
729 return ''.join(result)
731 def write(self, port, include_path=True):
732 records = include_path and [(_rec_tag_path, self._encode_path())] or []
733 records.extend([(_rec_tag_common_v2, self._encode_common()),
734 (_rec_tag_symlink_target,
735 self._encode_symlink_target()),
736 (_rec_tag_hardlink_target,
737 self._encode_hardlink_target()),
738 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
739 (_rec_tag_linux_attr, self._encode_linux_attr()),
740 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
741 for tag, data in records:
743 vint.write_vuint(port, tag)
744 vint.write_bvec(port, data)
745 vint.write_vuint(port, _rec_tag_end)
747 def encode(self, include_path=True):
749 self.write(port, include_path)
750 return port.getvalue()
754 # This method should either return a valid Metadata object,
755 # return None if there was no information at all (just a
756 # _rec_tag_end), throw EOFError if there was nothing at all to
757 # read, or throw an Exception if a valid object could not be
759 tag = vint.read_vuint(port)
760 if tag == _rec_tag_end:
762 try: # From here on, EOF is an error.
764 while True: # only exit is error (exception) or _rec_tag_end
765 if tag == _rec_tag_path:
766 result._load_path_rec(port)
767 elif tag == _rec_tag_common_v2:
768 result._load_common_rec(port)
769 elif tag == _rec_tag_symlink_target:
770 result._load_symlink_target_rec(port)
771 elif tag == _rec_tag_hardlink_target:
772 result._load_hardlink_target_rec(port)
773 elif tag == _rec_tag_posix1e_acl:
774 result._load_posix1e_acl_rec(port)
775 elif tag == _rec_tag_linux_attr:
776 result._load_linux_attr_rec(port)
777 elif tag == _rec_tag_linux_xattr:
778 result._load_linux_xattr_rec(port)
779 elif tag == _rec_tag_end:
781 elif tag == _rec_tag_common: # Should be very rare.
782 result._load_common_rec(port, legacy_format = True)
783 else: # unknown record
785 tag = vint.read_vuint(port)
787 raise Exception("EOF while reading Metadata")
790 return stat.S_ISDIR(self.mode)
792 def create_path(self, path, create_symlinks=True):
793 self._create_via_common_rec(path, create_symlinks=create_symlinks)
795 def apply_to_path(self, path=None, restore_numeric_ids=False):
796 # apply metadata to path -- file must exist
800 raise Exception('Metadata.apply_to_path() called with no path')
801 if not self._recognized_file_type():
802 add_error('not applying metadata to "%s"' % path
803 + ' with unrecognized mode "0x%x"\n' % self.mode)
805 num_ids = restore_numeric_ids
806 for apply_metadata in (self._apply_common_rec,
807 self._apply_posix1e_acl_rec,
808 self._apply_linux_attr_rec,
809 self._apply_linux_xattr_rec):
811 apply_metadata(path, restore_numeric_ids=num_ids)
812 except ApplyError, e:
815 def same_file(self, other):
816 """Compare this to other for equivalency. Return true if
817 their information implies they could represent the same file
818 on disk, in the hardlink sense. Assume they're both regular
820 return self._same_common(other) \
821 and self._same_hardlink_target(other) \
822 and self._same_posix1e_acl(other) \
823 and self._same_linux_attr(other) \
824 and self._same_linux_xattr(other)
827 def from_path(path, statinfo=None, archive_path=None,
828 save_symlinks=True, hardlink_target=None):
830 result.path = archive_path
831 st = statinfo or xstat.lstat(path)
832 result.size = st.st_size
833 result._add_common(path, st)
835 result._add_symlink_target(path, st)
836 result._add_hardlink_target(hardlink_target)
837 result._add_posix1e_acl(path, st)
838 result._add_linux_attr(path, st)
839 result._add_linux_xattr(path, st)
843 def save_tree(output_file, paths,
849 # Issue top-level rewrite warnings.
851 safe_path = _clean_up_path_for_archive(path)
852 if safe_path != path:
853 log('archiving "%s" as "%s"\n' % (path, safe_path))
857 safe_path = _clean_up_path_for_archive(p)
859 if stat.S_ISDIR(st.st_mode):
861 m = from_path(p, statinfo=st, archive_path=safe_path,
862 save_symlinks=save_symlinks)
864 print >> sys.stderr, m.path
865 m.write(output_file, include_path=write_paths)
867 start_dir = os.getcwd()
869 for (p, st) in recursive_dirlist(paths, xdev=xdev):
870 dirlist_dir = os.getcwd()
872 safe_path = _clean_up_path_for_archive(p)
873 m = from_path(p, statinfo=st, archive_path=safe_path,
874 save_symlinks=save_symlinks)
876 print >> sys.stderr, m.path
877 m.write(output_file, include_path=write_paths)
878 os.chdir(dirlist_dir)
883 def _set_up_path(meta, create_symlinks=True):
884 # Allow directories to exist as a special case -- might have
885 # been created by an earlier longer path.
889 parent = os.path.dirname(meta.path)
892 meta.create_path(meta.path, create_symlinks=create_symlinks)
895 all_fields = frozenset(['path',
912 def summary_str(meta, numeric_ids = False, classification = None,
913 human_readable = False):
915 """Return a string containing the "ls -l" style listing for meta.
916 Classification may be "all", "type", or None."""
917 user_str = group_str = size_or_dev_str = '?'
918 symlink_target = None
921 mode_str = xstat.mode_str(meta.mode)
922 symlink_target = meta.symlink_target
923 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
924 mtime_str = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
925 if meta.user and not numeric_ids:
927 elif meta.uid != None:
928 user_str = str(meta.uid)
929 if meta.group and not numeric_ids:
930 group_str = meta.group
931 elif meta.gid != None:
932 group_str = str(meta.gid)
933 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
935 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
937 elif meta.size != None:
939 size_or_dev_str = format_filesize(meta.size)
941 size_or_dev_str = str(meta.size)
943 size_or_dev_str = '-'
945 classification_str = \
946 xstat.classification_str(meta.mode, classification == 'all')
949 mtime_str = '????-??-?? ??:??'
950 classification_str = '?'
954 name += classification_str
956 name += ' -> ' + meta.symlink_target
958 return '%-10s %-11s %11s %16s %s' % (mode_str,
959 user_str + "/" + group_str,
965 def detailed_str(meta, fields = None):
966 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
967 # 0", "link-target:", etc.
973 path = meta.path or ''
974 result.append('path: ' + path)
976 result.append('mode: %s (%s)' % (oct(meta.mode),
977 xstat.mode_str(meta.mode)))
978 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
979 result.append('link-target: ' + meta.symlink_target)
982 result.append('rdev: %d,%d' % (os.major(meta.rdev),
983 os.minor(meta.rdev)))
985 result.append('rdev: 0')
986 if 'size' in fields and meta.size:
987 result.append('size: ' + str(meta.size))
989 result.append('uid: ' + str(meta.uid))
991 result.append('gid: ' + str(meta.gid))
993 result.append('user: ' + meta.user)
994 if 'group' in fields:
995 result.append('group: ' + meta.group)
996 if 'atime' in fields:
997 # If we don't have xstat.lutime, that means we have to use
998 # utime(), and utime() has no way to set the mtime/atime of a
999 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1000 # so let's not report it. (That way scripts comparing
1001 # before/after won't trigger.)
1002 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1003 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1005 result.append('atime: 0')
1006 if 'mtime' in fields:
1007 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1008 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1010 result.append('mtime: 0')
1011 if 'ctime' in fields:
1012 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1013 if 'linux-attr' in fields and meta.linux_attr:
1014 result.append('linux-attr: ' + hex(meta.linux_attr))
1015 if 'linux-xattr' in fields and meta.linux_xattr:
1016 for name, value in meta.linux_xattr:
1017 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1018 if 'posix1e-acl' in fields and meta.posix1e_acl:
1019 acl = meta.posix1e_acl[0]
1020 result.append('posix1e-acl: ' + acl + '\n')
1021 if stat.S_ISDIR(meta.mode):
1022 def_acl = meta.posix1e_acl[2]
1023 result.append('posix1e-acl-default: ' + def_acl + '\n')
1024 return '\n'.join(result)
1027 class _ArchiveIterator:
1030 return Metadata.read(self._file)
1032 raise StopIteration()
1037 def __init__(self, file):
1041 def display_archive(file):
1044 for meta in _ArchiveIterator(file):
1047 print detailed_str(meta)
1050 for meta in _ArchiveIterator(file):
1051 print summary_str(meta)
1053 for meta in _ArchiveIterator(file):
1055 print >> sys.stderr, \
1056 'bup: no metadata path, but asked to only display path', \
1057 '(increase verbosity?)'
1062 def start_extract(file, create_symlinks=True):
1063 for meta in _ArchiveIterator(file):
1064 if not meta: # Hit end record.
1067 print >> sys.stderr, meta.path
1068 xpath = _clean_up_extract_path(meta.path)
1070 add_error(Exception('skipping risky path "%s"' % meta.path))
1073 _set_up_path(meta, create_symlinks=create_symlinks)
1076 def finish_extract(file, restore_numeric_ids=False):
1078 for meta in _ArchiveIterator(file):
1079 if not meta: # Hit end record.
1081 xpath = _clean_up_extract_path(meta.path)
1083 add_error(Exception('skipping risky path "%s"' % dir.path))
1085 if os.path.isdir(meta.path):
1086 all_dirs.append(meta)
1089 print >> sys.stderr, meta.path
1090 meta.apply_to_path(path=xpath,
1091 restore_numeric_ids=restore_numeric_ids)
1092 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1093 for dir in all_dirs:
1094 # Don't need to check xpath -- won't be in all_dirs if not OK.
1095 xpath = _clean_up_extract_path(dir.path)
1097 print >> sys.stderr, dir.path
1098 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1101 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1102 # For now, just store all the directories and handle them last,
1105 for meta in _ArchiveIterator(file):
1106 if not meta: # Hit end record.
1108 xpath = _clean_up_extract_path(meta.path)
1110 add_error(Exception('skipping risky path "%s"' % meta.path))
1114 print >> sys.stderr, '+', meta.path
1115 _set_up_path(meta, create_symlinks=create_symlinks)
1116 if os.path.isdir(meta.path):
1117 all_dirs.append(meta)
1120 print >> sys.stderr, '=', meta.path
1121 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1122 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1123 for dir in all_dirs:
1124 # Don't need to check xpath -- won't be in all_dirs if not OK.
1125 xpath = _clean_up_extract_path(dir.path)
1127 print >> sys.stderr, '=', xpath
1128 # Shouldn't have to check for risky paths here (omitted above).
1129 dir.apply_to_path(path=dir.path,
1130 restore_numeric_ids=restore_numeric_ids)