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.
8 from __future__ import absolute_import
9 from copy import deepcopy
10 from errno import EACCES, EINVAL, ENOTTY, ENOSYS, EOPNOTSUPP
11 from io import BytesIO
12 from time import gmtime, strftime
13 import errno, os, sys, stat, time, pwd, grp, socket, struct
15 from bup import vint, xstat
16 from bup.drecurse import recursive_dirlist
17 from bup.helpers import add_error, mkdirp, log, is_superuser, format_filesize
18 from bup.helpers import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
19 from bup.xstat import utime, lutime
22 if sys.platform.startswith('linux'):
26 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
30 except AttributeError:
31 log('Warning: python-xattr module is too old; '
32 'install python-pyxattr instead.\n')
36 if not (sys.platform.startswith('cygwin') \
37 or sys.platform.startswith('darwin') \
38 or sys.platform.startswith('netbsd')):
42 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
45 from bup._helpers import get_linux_file_attr, set_linux_file_attr
47 # No need for a warning here; the only reason they won't exist is that we're
48 # not on Linux, in which case files don't have any linux attrs anyway, so
49 # lacking the functions isn't a problem.
50 get_linux_file_attr = set_linux_file_attr = None
53 # See the bup_get_linux_file_attr() comments.
54 _suppress_linux_file_attr = \
55 sys.byteorder == 'big' and struct.calcsize('@l') > struct.calcsize('@i')
57 def check_linux_file_attr_api():
58 global get_linux_file_attr, set_linux_file_attr
59 if not (get_linux_file_attr or set_linux_file_attr):
61 if _suppress_linux_file_attr:
62 log('Warning: Linux attr support disabled (see "bup help index").\n')
63 get_linux_file_attr = set_linux_file_attr = None
66 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
68 # Q: Consider hardlink support?
69 # Q: Is it OK to store raw linux attr (chattr) flags?
70 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
71 # Q: Is the application of posix1e has_extended() correct?
72 # Q: Is one global --numeric-ids argument sufficient?
73 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
74 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
76 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
77 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
78 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
79 # FIXME: Consider pack('vvvvsss', ...) optimization.
83 # osx (varies between hfs and hfs+):
84 # type - regular dir char block fifo socket ...
85 # perms - rwxrwxrwxsgt
86 # times - ctime atime mtime
89 # hard-link-info (hfs+ only)
92 # attributes-osx see chflags
98 # type - regular dir ...
99 # times - creation, modification, posix change, access
102 # attributes - see attrib
104 # forks (alternate data streams)
108 # type - regular dir ...
109 # perms - rwxrwxrwx (maybe - see wikipedia)
110 # times - creation, modification, access
111 # attributes - see attrib
115 _have_lchmod = hasattr(os, 'lchmod')
118 def _clean_up_path_for_archive(p):
119 # Not the most efficient approach.
122 # Take everything after any '/../'.
123 pos = result.rfind('/../')
125 result = result[result.rfind('/../') + 4:]
127 # Take everything after any remaining '../'.
128 if result.startswith("../"):
131 # Remove any '/./' sequences.
132 pos = result.find('/./')
134 result = result[0:pos] + '/' + result[pos + 3:]
135 pos = result.find('/./')
137 # Remove any leading '/'s.
138 result = result.lstrip('/')
140 # Replace '//' with '/' everywhere.
141 pos = result.find('//')
143 result = result[0:pos] + '/' + result[pos + 2:]
144 pos = result.find('//')
146 # Take everything after any remaining './'.
147 if result.startswith('./'):
150 # Take everything before any remaining '/.'.
151 if result.endswith('/.'):
154 if result == '' or result.endswith('/..'):
161 if p.startswith('/'):
163 if p.find('/../') != -1:
165 if p.startswith('../'):
167 if p.endswith('/..'):
172 def _clean_up_extract_path(p):
173 result = p.lstrip('/')
176 elif _risky_path(result):
182 # These tags are currently conceptually private to Metadata, and they
183 # must be unique, and must *never* be changed.
186 _rec_tag_common = 2 # times, user, group, type, perms, etc. (legacy/broken)
187 _rec_tag_symlink_target = 3
188 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
189 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
190 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
191 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
192 _rec_tag_hardlink_target = 8 # hard link target path
193 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
195 _warned_about_attr_einval = None
198 class ApplyError(Exception):
199 # Thrown when unable to apply any given bit of metadata to a path.
204 # Metadata is stored as a sequence of tagged binary records. Each
205 # record will have some subset of add, encode, load, create, and
206 # apply methods, i.e. _add_foo...
208 # We do allow an "empty" object as a special case, i.e. no
209 # records. One can be created by trying to write Metadata(), and
210 # for such an object, read() will return None. This is used by
211 # "bup save", for example, as a placeholder in cases where
214 # NOTE: if any relevant fields are added or removed, be sure to
215 # update same_file() below.
219 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
220 # must be non-negative and < 10**9.
222 def _add_common(self, path, st):
223 assert(st.st_uid >= 0)
224 assert(st.st_gid >= 0)
227 self.atime = st.st_atime
228 self.mtime = st.st_mtime
229 self.ctime = st.st_ctime
230 self.user = self.group = ''
231 entry = pwd_from_uid(st.st_uid)
233 self.user = entry.pw_name
234 entry = grp_from_gid(st.st_gid)
236 self.group = entry.gr_name
237 self.mode = st.st_mode
238 # Only collect st_rdev if we might need it for a mknod()
239 # during restore. On some platforms (i.e. kFreeBSD), it isn't
240 # stable for other file types. For example "cp -a" will
241 # change it for a plain file.
242 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
243 self.rdev = st.st_rdev
247 def _same_common(self, other):
248 """Return true or false to indicate similarity in the hardlink sense."""
249 return self.uid == other.uid \
250 and self.gid == other.gid \
251 and self.rdev == other.rdev \
252 and self.mtime == other.mtime \
253 and self.ctime == other.ctime \
254 and self.user == other.user \
255 and self.group == other.group
257 def _encode_common(self):
260 atime = xstat.nsecs_to_timespec(self.atime)
261 mtime = xstat.nsecs_to_timespec(self.mtime)
262 ctime = xstat.nsecs_to_timespec(self.ctime)
263 result = vint.pack('vvsvsvvVvVvV',
278 def _load_common_rec(self, port, legacy_format=False):
279 unpack_fmt = 'vvsvsvvVvVvV'
281 unpack_fmt = 'VVsVsVvVvVvV'
282 data = vint.read_bvec(port)
294 ctime_ns) = vint.unpack(unpack_fmt, data)
295 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
296 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
297 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
299 def _recognized_file_type(self):
300 return stat.S_ISREG(self.mode) \
301 or stat.S_ISDIR(self.mode) \
302 or stat.S_ISCHR(self.mode) \
303 or stat.S_ISBLK(self.mode) \
304 or stat.S_ISFIFO(self.mode) \
305 or stat.S_ISSOCK(self.mode) \
306 or stat.S_ISLNK(self.mode)
308 def _create_via_common_rec(self, path, create_symlinks=True):
310 raise ApplyError('no metadata - cannot create path ' + path)
312 # If the path already exists and is a dir, try rmdir.
313 # If the path already exists and is anything else, try unlink.
316 st = xstat.lstat(path)
318 if e.errno != errno.ENOENT:
321 if stat.S_ISDIR(st.st_mode):
325 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
326 msg = 'refusing to overwrite non-empty dir ' + path
332 if stat.S_ISREG(self.mode):
333 assert(self._recognized_file_type())
334 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
336 elif stat.S_ISDIR(self.mode):
337 assert(self._recognized_file_type())
338 os.mkdir(path, 0o700)
339 elif stat.S_ISCHR(self.mode):
340 assert(self._recognized_file_type())
341 os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
342 elif stat.S_ISBLK(self.mode):
343 assert(self._recognized_file_type())
344 os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
345 elif stat.S_ISFIFO(self.mode):
346 assert(self._recognized_file_type())
347 os.mknod(path, 0o600 | stat.S_IFIFO)
348 elif stat.S_ISSOCK(self.mode):
350 os.mknod(path, 0o600 | stat.S_IFSOCK)
352 if e.errno in (errno.EINVAL, errno.EPERM):
353 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
357 elif stat.S_ISLNK(self.mode):
358 assert(self._recognized_file_type())
359 if self.symlink_target and create_symlinks:
360 # on MacOS, symlink() permissions depend on umask, and there's
361 # no way to chown a symlink after creating it, so we have to
363 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
365 os.symlink(self.symlink_target, path)
368 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
370 assert(not self._recognized_file_type())
371 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
374 def _apply_common_rec(self, path, restore_numeric_ids=False):
376 raise ApplyError('no metadata - cannot apply to ' + path)
378 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
379 # EACCES errors at this stage are fatal for the current path.
380 if lutime and stat.S_ISLNK(self.mode):
382 lutime(path, (self.atime, self.mtime))
384 if e.errno == errno.EACCES:
385 raise ApplyError('lutime: %s' % e)
390 utime(path, (self.atime, self.mtime))
392 if e.errno == errno.EACCES:
393 raise ApplyError('utime: %s' % e)
397 uid = gid = -1 # By default, do nothing.
401 if not restore_numeric_ids:
402 if self.uid != 0 and self.user:
403 entry = pwd_from_name(self.user)
406 if self.gid != 0 and self.group:
407 entry = grp_from_name(self.group)
410 else: # not superuser - only consider changing the group/gid
411 user_gids = os.getgroups()
412 if self.gid in user_gids:
414 if not restore_numeric_ids and self.gid != 0:
415 # The grp might not exist on the local system.
416 grps = filter(None, [grp_from_gid(x) for x in user_gids])
417 if self.group in [x.gr_name for x in grps]:
418 g = grp_from_name(self.group)
422 if uid != -1 or gid != -1:
424 os.lchown(path, uid, gid)
426 if e.errno == errno.EPERM:
427 add_error('lchown: %s' % e)
428 elif sys.platform.startswith('cygwin') \
429 and e.errno == errno.EINVAL:
430 add_error('lchown: unknown uid/gid (%d/%d) for %s'
437 os.lchmod(path, stat.S_IMODE(self.mode))
438 except errno.ENOSYS: # Function not implemented
440 elif not stat.S_ISLNK(self.mode):
441 os.chmod(path, stat.S_IMODE(self.mode))
446 def _encode_path(self):
448 return vint.pack('s', self.path)
452 def _load_path_rec(self, port):
453 self.path = vint.unpack('s', vint.read_bvec(port))[0]
458 def _add_symlink_target(self, path, st):
460 if stat.S_ISLNK(st.st_mode):
461 self.symlink_target = os.readlink(path)
463 add_error('readlink: %s' % e)
465 def _encode_symlink_target(self):
466 return self.symlink_target
468 def _load_symlink_target_rec(self, port):
469 target = vint.read_bvec(port)
470 self.symlink_target = target
471 self.size = len(target)
476 def _add_hardlink_target(self, target):
477 self.hardlink_target = target
479 def _same_hardlink_target(self, other):
480 """Return true or false to indicate similarity in the hardlink sense."""
481 return self.hardlink_target == other.hardlink_target
483 def _encode_hardlink_target(self):
484 return self.hardlink_target
486 def _load_hardlink_target_rec(self, port):
487 self.hardlink_target = vint.read_bvec(port)
490 ## POSIX1e ACL records
492 # Recorded as a list:
493 # [txt_id_acl, num_id_acl]
494 # or, if a directory:
495 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
496 # The numeric/text distinction only matters when reading/restoring
498 def _add_posix1e_acl(self, path, st):
499 if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
501 if not stat.S_ISLNK(st.st_mode):
505 if posix1e.has_extended(path):
506 acl = posix1e.ACL(file=path)
507 acls = [acl, acl] # txt and num are the same
508 if stat.S_ISDIR(st.st_mode):
509 def_acl = posix1e.ACL(filedef=path)
510 def_acls = [def_acl, def_acl]
511 except EnvironmentError as e:
512 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
515 txt_flags = posix1e.TEXT_ABBREVIATE
516 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
517 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
518 acls[1].to_any_text('', '\n', num_flags)]
520 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
521 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
522 self.posix1e_acl = acl_rep
524 def _same_posix1e_acl(self, other):
525 """Return true or false to indicate similarity in the hardlink sense."""
526 return self.posix1e_acl == other.posix1e_acl
528 def _encode_posix1e_acl(self):
529 # Encode as two strings (w/default ACL string possibly empty).
531 acls = self.posix1e_acl
533 acls.extend(['', ''])
534 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
538 def _load_posix1e_acl_rec(self, port):
539 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
541 acl_rep = acl_rep[:2]
542 self.posix1e_acl = acl_rep
544 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
545 def apply_acl(acl_rep, kind):
547 acl = posix1e.ACL(text = acl_rep)
550 # pylibacl appears to return an IOError with errno
551 # set to 0 if a group referred to by the ACL rep
552 # doesn't exist on the current system.
553 raise ApplyError("POSIX1e ACL: can't create %r for %r"
558 acl.applyto(path, kind)
560 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
561 raise ApplyError('POSIX1e ACL applyto: %s' % e)
567 add_error("%s: can't restore ACLs; posix1e support missing.\n"
571 acls = self.posix1e_acl
573 if restore_numeric_ids:
574 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
576 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
577 if restore_numeric_ids:
578 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
580 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
583 ## Linux attributes (lsattr(1), chattr(1))
585 def _add_linux_attr(self, path, st):
586 check_linux_file_attr_api()
587 if not get_linux_file_attr: return
588 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
590 attr = get_linux_file_attr(path)
592 self.linux_attr = attr
594 if e.errno == errno.EACCES:
595 add_error('read Linux attr: %s' % e)
596 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
597 # Assume filesystem doesn't support attrs.
599 elif e.errno == EINVAL:
600 global _warned_about_attr_einval
601 if not _warned_about_attr_einval:
602 log("Ignoring attr EINVAL;"
603 + " if you're not using ntfs-3g, please report: "
605 _warned_about_attr_einval = True
610 def _same_linux_attr(self, other):
611 """Return true or false to indicate similarity in the hardlink sense."""
612 return self.linux_attr == other.linux_attr
614 def _encode_linux_attr(self):
616 return vint.pack('V', self.linux_attr)
620 def _load_linux_attr_rec(self, port):
621 data = vint.read_bvec(port)
622 self.linux_attr = vint.unpack('V', data)[0]
624 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
626 check_linux_file_attr_api()
627 if not set_linux_file_attr:
628 add_error("%s: can't restore linuxattrs: "
629 "linuxattr support missing.\n" % path)
632 set_linux_file_attr(path, self.linux_attr)
634 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
635 raise ApplyError('Linux chattr: %s (0x%s)'
636 % (e, hex(self.linux_attr)))
637 elif e.errno == EINVAL:
638 msg = "if you're not using ntfs-3g, please report"
639 raise ApplyError('Linux chattr: %s (0x%s) (%s)'
640 % (e, hex(self.linux_attr), msg))
645 ## Linux extended attributes (getfattr(1), setfattr(1))
647 def _add_linux_xattr(self, path, st):
650 self.linux_xattr = xattr.get_all(path, nofollow=True)
651 except EnvironmentError as e:
652 if e.errno != errno.EOPNOTSUPP:
655 def _same_linux_xattr(self, other):
656 """Return true or false to indicate similarity in the hardlink sense."""
657 return self.linux_xattr == other.linux_xattr
659 def _encode_linux_xattr(self):
661 result = vint.pack('V', len(self.linux_xattr))
662 for name, value in self.linux_xattr:
663 result += vint.pack('ss', name, value)
668 def _load_linux_xattr_rec(self, file):
669 data = vint.read_bvec(file)
670 memfile = BytesIO(data)
672 for i in range(vint.read_vuint(memfile)):
673 key = vint.read_bvec(memfile)
674 value = vint.read_bvec(memfile)
675 result.append((key, value))
676 self.linux_xattr = result
678 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
681 add_error("%s: can't restore xattr; xattr support missing.\n"
684 if not self.linux_xattr:
687 existing_xattrs = set(xattr.list(path, nofollow=True))
689 if e.errno == errno.EACCES:
690 raise ApplyError('xattr.set %r: %s' % (path, e))
693 for k, v in self.linux_xattr:
694 if k not in existing_xattrs \
695 or v != xattr.get(path, k, nofollow=True):
697 xattr.set(path, k, v, nofollow=True)
699 if e.errno == errno.EPERM \
700 or e.errno == errno.EOPNOTSUPP:
701 raise ApplyError('xattr.set %r: %s' % (path, e))
704 existing_xattrs -= frozenset([k])
705 for k in existing_xattrs:
707 xattr.remove(path, k, nofollow=True)
709 if e.errno in (errno.EPERM, errno.EACCES):
710 raise ApplyError('xattr.remove %r: %s' % (path, e))
715 self.mode = self.uid = self.gid = self.user = self.group = None
716 self.atime = self.mtime = self.ctime = None
720 self.symlink_target = None
721 self.hardlink_target = None
722 self.linux_attr = None
723 self.linux_xattr = None
724 self.posix1e_acl = None
726 def __eq__(self, other):
727 if not isinstance(other, Metadata): return False
728 if self.mode != other.mode: return False
729 if self.mtime != other.mtime: return False
730 if self.ctime != other.ctime: return False
731 if self.atime != other.atime: return False
732 if self.path != other.path: return False
733 if self.uid != other.uid: return False
734 if self.gid != other.gid: return False
735 if self.size != other.size: return False
736 if self.user != other.user: return False
737 if self.group != other.group: return False
738 if self.symlink_target != other.symlink_target: return False
739 if self.hardlink_target != other.hardlink_target: return False
740 if self.linux_attr != other.linux_attr: return False
741 if self.posix1e_acl != other.posix1e_acl: return False
744 def __ne__(self, other):
745 return not self.__eq__(other)
748 return hash((self.mode,
759 self.hardlink_target,
764 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
765 if self.path is not None:
766 result += ' path:' + repr(self.path)
767 if self.mode is not None:
768 result += ' mode:' + repr(xstat.mode_str(self.mode)
769 + '(%s)' % oct(self.mode))
770 if self.uid is not None:
771 result += ' uid:' + str(self.uid)
772 if self.gid is not None:
773 result += ' gid:' + str(self.gid)
774 if self.user is not None:
775 result += ' user:' + repr(self.user)
776 if self.group is not None:
777 result += ' group:' + repr(self.group)
778 if self.size is not None:
779 result += ' size:' + repr(self.size)
780 for name, val in (('atime', self.atime),
781 ('mtime', self.mtime),
782 ('ctime', self.ctime)):
784 result += ' %s:%r (%d)' \
786 strftime('%Y-%m-%d %H:%M %z',
787 gmtime(xstat.fstime_floor_secs(val))),
790 return ''.join(result)
792 def write(self, port, include_path=True):
793 records = include_path and [(_rec_tag_path, self._encode_path())] or []
794 records.extend([(_rec_tag_common_v2, self._encode_common()),
795 (_rec_tag_symlink_target,
796 self._encode_symlink_target()),
797 (_rec_tag_hardlink_target,
798 self._encode_hardlink_target()),
799 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
800 (_rec_tag_linux_attr, self._encode_linux_attr()),
801 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
802 for tag, data in records:
804 vint.write_vuint(port, tag)
805 vint.write_bvec(port, data)
806 vint.write_vuint(port, _rec_tag_end)
808 def encode(self, include_path=True):
810 self.write(port, include_path)
811 return port.getvalue()
814 return deepcopy(self)
818 # This method should either return a valid Metadata object,
819 # return None if there was no information at all (just a
820 # _rec_tag_end), throw EOFError if there was nothing at all to
821 # read, or throw an Exception if a valid object could not be
823 tag = vint.read_vuint(port)
824 if tag == _rec_tag_end:
826 try: # From here on, EOF is an error.
828 while True: # only exit is error (exception) or _rec_tag_end
829 if tag == _rec_tag_path:
830 result._load_path_rec(port)
831 elif tag == _rec_tag_common_v2:
832 result._load_common_rec(port)
833 elif tag == _rec_tag_symlink_target:
834 result._load_symlink_target_rec(port)
835 elif tag == _rec_tag_hardlink_target:
836 result._load_hardlink_target_rec(port)
837 elif tag == _rec_tag_posix1e_acl:
838 result._load_posix1e_acl_rec(port)
839 elif tag == _rec_tag_linux_attr:
840 result._load_linux_attr_rec(port)
841 elif tag == _rec_tag_linux_xattr:
842 result._load_linux_xattr_rec(port)
843 elif tag == _rec_tag_end:
845 elif tag == _rec_tag_common: # Should be very rare.
846 result._load_common_rec(port, legacy_format = True)
847 else: # unknown record
849 tag = vint.read_vuint(port)
851 raise Exception("EOF while reading Metadata")
854 return stat.S_ISDIR(self.mode)
856 def create_path(self, path, create_symlinks=True):
857 self._create_via_common_rec(path, create_symlinks=create_symlinks)
859 def apply_to_path(self, path=None, restore_numeric_ids=False):
860 # apply metadata to path -- file must exist
864 raise Exception('Metadata.apply_to_path() called with no path')
865 if not self._recognized_file_type():
866 add_error('not applying metadata to "%s"' % path
867 + ' with unrecognized mode "0x%x"\n' % self.mode)
869 num_ids = restore_numeric_ids
870 for apply_metadata in (self._apply_common_rec,
871 self._apply_posix1e_acl_rec,
872 self._apply_linux_attr_rec,
873 self._apply_linux_xattr_rec):
875 apply_metadata(path, restore_numeric_ids=num_ids)
876 except ApplyError as e:
879 def same_file(self, other):
880 """Compare this to other for equivalency. Return true if
881 their information implies they could represent the same file
882 on disk, in the hardlink sense. Assume they're both regular
884 return self._same_common(other) \
885 and self._same_hardlink_target(other) \
886 and self._same_posix1e_acl(other) \
887 and self._same_linux_attr(other) \
888 and self._same_linux_xattr(other)
891 def from_path(path, statinfo=None, archive_path=None,
892 save_symlinks=True, hardlink_target=None):
894 result.path = archive_path
895 st = statinfo or xstat.lstat(path)
896 result.size = st.st_size
897 result._add_common(path, st)
899 result._add_symlink_target(path, st)
900 result._add_hardlink_target(hardlink_target)
901 result._add_posix1e_acl(path, st)
902 result._add_linux_attr(path, st)
903 result._add_linux_xattr(path, st)
907 def save_tree(output_file, paths,
913 # Issue top-level rewrite warnings.
915 safe_path = _clean_up_path_for_archive(path)
916 if safe_path != path:
917 log('archiving "%s" as "%s"\n' % (path, safe_path))
921 safe_path = _clean_up_path_for_archive(p)
923 if stat.S_ISDIR(st.st_mode):
925 m = from_path(p, statinfo=st, archive_path=safe_path,
926 save_symlinks=save_symlinks)
928 print >> sys.stderr, m.path
929 m.write(output_file, include_path=write_paths)
931 start_dir = os.getcwd()
933 for (p, st) in recursive_dirlist(paths, xdev=xdev):
934 dirlist_dir = os.getcwd()
936 safe_path = _clean_up_path_for_archive(p)
937 m = from_path(p, statinfo=st, archive_path=safe_path,
938 save_symlinks=save_symlinks)
940 print >> sys.stderr, m.path
941 m.write(output_file, include_path=write_paths)
942 os.chdir(dirlist_dir)
947 def _set_up_path(meta, create_symlinks=True):
948 # Allow directories to exist as a special case -- might have
949 # been created by an earlier longer path.
953 parent = os.path.dirname(meta.path)
956 meta.create_path(meta.path, create_symlinks=create_symlinks)
959 all_fields = frozenset(['path',
976 def summary_str(meta, numeric_ids = False, classification = None,
977 human_readable = False):
979 """Return a string containing the "ls -l" style listing for meta.
980 Classification may be "all", "type", or None."""
981 user_str = group_str = size_or_dev_str = '?'
982 symlink_target = None
985 mode_str = xstat.mode_str(meta.mode)
986 symlink_target = meta.symlink_target
987 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
988 mtime_str = strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
989 if meta.user and not numeric_ids:
991 elif meta.uid != None:
992 user_str = str(meta.uid)
993 if meta.group and not numeric_ids:
994 group_str = meta.group
995 elif meta.gid != None:
996 group_str = str(meta.gid)
997 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
999 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
1000 os.minor(meta.rdev))
1001 elif meta.size != None:
1003 size_or_dev_str = format_filesize(meta.size)
1005 size_or_dev_str = str(meta.size)
1007 size_or_dev_str = '-'
1009 classification_str = \
1010 xstat.classification_str(meta.mode, classification == 'all')
1013 mtime_str = '????-??-?? ??:??'
1014 classification_str = '?'
1018 name += classification_str
1020 name += ' -> ' + meta.symlink_target
1022 return '%-10s %-11s %11s %16s %s' % (mode_str,
1023 user_str + "/" + group_str,
1029 def detailed_str(meta, fields = None):
1030 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
1031 # 0", "link-target:", etc.
1036 if 'path' in fields:
1037 path = meta.path or ''
1038 result.append('path: ' + path)
1039 if 'mode' in fields:
1040 result.append('mode: %s (%s)' % (oct(meta.mode),
1041 xstat.mode_str(meta.mode)))
1042 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1043 result.append('link-target: ' + meta.symlink_target)
1044 if 'rdev' in fields:
1046 result.append('rdev: %d,%d' % (os.major(meta.rdev),
1047 os.minor(meta.rdev)))
1049 result.append('rdev: 0')
1050 if 'size' in fields and meta.size:
1051 result.append('size: ' + str(meta.size))
1053 result.append('uid: ' + str(meta.uid))
1055 result.append('gid: ' + str(meta.gid))
1056 if 'user' in fields:
1057 result.append('user: ' + meta.user)
1058 if 'group' in fields:
1059 result.append('group: ' + meta.group)
1060 if 'atime' in fields:
1061 # If we don't have xstat.lutime, that means we have to use
1062 # utime(), and utime() has no way to set the mtime/atime of a
1063 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1064 # so let's not report it. (That way scripts comparing
1065 # before/after won't trigger.)
1066 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1067 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1069 result.append('atime: 0')
1070 if 'mtime' in fields:
1071 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1072 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1074 result.append('mtime: 0')
1075 if 'ctime' in fields:
1076 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1077 if 'linux-attr' in fields and meta.linux_attr:
1078 result.append('linux-attr: ' + hex(meta.linux_attr))
1079 if 'linux-xattr' in fields and meta.linux_xattr:
1080 for name, value in meta.linux_xattr:
1081 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1082 if 'posix1e-acl' in fields and meta.posix1e_acl:
1083 acl = meta.posix1e_acl[0]
1084 result.append('posix1e-acl: ' + acl + '\n')
1085 if stat.S_ISDIR(meta.mode):
1086 def_acl = meta.posix1e_acl[2]
1087 result.append('posix1e-acl-default: ' + def_acl + '\n')
1088 return '\n'.join(result)
1091 class _ArchiveIterator:
1094 return Metadata.read(self._file)
1096 raise StopIteration()
1101 def __init__(self, file):
1105 def display_archive(file):
1108 for meta in _ArchiveIterator(file):
1111 print detailed_str(meta)
1114 for meta in _ArchiveIterator(file):
1115 print summary_str(meta)
1117 for meta in _ArchiveIterator(file):
1119 print >> sys.stderr, \
1120 'bup: no metadata path, but asked to only display path', \
1121 '(increase verbosity?)'
1126 def start_extract(file, create_symlinks=True):
1127 for meta in _ArchiveIterator(file):
1128 if not meta: # Hit end record.
1131 print >> sys.stderr, meta.path
1132 xpath = _clean_up_extract_path(meta.path)
1134 add_error(Exception('skipping risky path "%s"' % meta.path))
1137 _set_up_path(meta, create_symlinks=create_symlinks)
1140 def finish_extract(file, restore_numeric_ids=False):
1142 for meta in _ArchiveIterator(file):
1143 if not meta: # Hit end record.
1145 xpath = _clean_up_extract_path(meta.path)
1147 add_error(Exception('skipping risky path "%s"' % dir.path))
1149 if os.path.isdir(meta.path):
1150 all_dirs.append(meta)
1153 print >> sys.stderr, meta.path
1154 meta.apply_to_path(path=xpath,
1155 restore_numeric_ids=restore_numeric_ids)
1156 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1157 for dir in all_dirs:
1158 # Don't need to check xpath -- won't be in all_dirs if not OK.
1159 xpath = _clean_up_extract_path(dir.path)
1161 print >> sys.stderr, dir.path
1162 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1165 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1166 # For now, just store all the directories and handle them last,
1169 for meta in _ArchiveIterator(file):
1170 if not meta: # Hit end record.
1172 xpath = _clean_up_extract_path(meta.path)
1174 add_error(Exception('skipping risky path "%s"' % meta.path))
1178 print >> sys.stderr, '+', meta.path
1179 _set_up_path(meta, create_symlinks=create_symlinks)
1180 if os.path.isdir(meta.path):
1181 all_dirs.append(meta)
1184 print >> sys.stderr, '=', meta.path
1185 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1186 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1187 for dir in all_dirs:
1188 # Don't need to check xpath -- won't be in all_dirs if not OK.
1189 xpath = _clean_up_extract_path(dir.path)
1191 print >> sys.stderr, '=', xpath
1192 # Shouldn't have to check for risky paths here (omitted above).
1193 dir.apply_to_path(path=dir.path,
1194 restore_numeric_ids=restore_numeric_ids)