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, print_function
9 from binascii import hexlify
10 from copy import deepcopy
11 from errno import EACCES, EINVAL, ENOTTY, ENOSYS, EOPNOTSUPP
12 from io import BytesIO
13 from time import gmtime, strftime
14 import errno, os, sys, stat, time, pwd, grp, socket, struct
16 from bup import compat, vint, xstat
17 from bup.compat import py_maj
18 from bup.drecurse import recursive_dirlist
19 from bup.helpers import add_error, mkdirp, log, is_superuser, format_filesize
20 from bup.io import path_msg
21 from bup.pwdgrp import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
22 from bup.xstat import utime, lutime
25 if sys.platform.startswith('linux'):
26 # prefer python-pyxattr (it's a lot faster), but fall back to python-xattr
27 # as the two are incompatible and only one can be installed on a system
31 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
32 if xattr and getattr(xattr, 'get_all', None) is None:
34 from xattr import pyxattr_compat as xattr
35 if not isinstance(xattr.NS_USER, bytes):
40 log('Warning: python-xattr module is too old; '
41 'upgrade or install python-pyxattr instead.\n')
44 from bup._helpers import read_acl, apply_acl
46 read_acl = apply_acl = None
49 from bup._helpers import get_linux_file_attr, set_linux_file_attr
51 # No need for a warning here; the only reason they won't exist is that we're
52 # not on Linux, in which case files don't have any linux attrs anyway, so
53 # lacking the functions isn't a problem.
54 get_linux_file_attr = set_linux_file_attr = None
57 # See the bup_get_linux_file_attr() comments.
58 _suppress_linux_file_attr = \
59 sys.byteorder == 'big' and struct.calcsize('@l') > struct.calcsize('@i')
61 def check_linux_file_attr_api():
62 global get_linux_file_attr, set_linux_file_attr
63 if not (get_linux_file_attr or set_linux_file_attr):
65 if _suppress_linux_file_attr:
66 log('Warning: Linux attr support disabled (see "bup help index").\n')
67 get_linux_file_attr = set_linux_file_attr = None
70 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
72 # Q: Consider hardlink support?
73 # Q: Is it OK to store raw linux attr (chattr) flags?
74 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
75 # Q: Is the application of posix1e has_extended() correct?
76 # Q: Is one global --numeric-ids argument sufficient?
77 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
78 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
80 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
81 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
82 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
83 # FIXME: Consider pack('vvvvsss', ...) optimization.
87 # osx (varies between hfs and hfs+):
88 # type - regular dir char block fifo socket ...
89 # perms - rwxrwxrwxsgt
90 # times - ctime atime mtime
93 # hard-link-info (hfs+ only)
96 # attributes-osx see chflags
102 # type - regular dir ...
103 # times - creation, modification, posix change, access
106 # attributes - see attrib
108 # forks (alternate data streams)
112 # type - regular dir ...
113 # perms - rwxrwxrwx (maybe - see wikipedia)
114 # times - creation, modification, access
115 # attributes - see attrib
119 _have_lchmod = hasattr(os, 'lchmod')
122 def _clean_up_path_for_archive(p):
123 # Not the most efficient approach.
126 # Take everything after any '/../'.
127 pos = result.rfind(b'/../')
129 result = result[result.rfind(b'/../') + 4:]
131 # Take everything after any remaining '../'.
132 if result.startswith(b"../"):
135 # Remove any '/./' sequences.
136 pos = result.find(b'/./')
138 result = result[0:pos] + b'/' + result[pos + 3:]
139 pos = result.find(b'/./')
141 # Remove any leading '/'s.
142 result = result.lstrip(b'/')
144 # Replace '//' with '/' everywhere.
145 pos = result.find(b'//')
147 result = result[0:pos] + b'/' + result[pos + 2:]
148 pos = result.find(b'//')
150 # Take everything after any remaining './'.
151 if result.startswith(b'./'):
154 # Take everything before any remaining '/.'.
155 if result.endswith(b'/.'):
158 if result == b'' or result.endswith(b'/..'):
165 if p.startswith(b'/'):
167 if p.find(b'/../') != -1:
169 if p.startswith(b'../'):
171 if p.endswith(b'/..'):
176 def _clean_up_extract_path(p):
177 result = p.lstrip(b'/')
180 elif _risky_path(result):
186 # These tags are currently conceptually private to Metadata, and they
187 # must be unique, and must *never* be changed.
190 _rec_tag_common_v1 = 2 # times, user, group, type, perms, etc. (legacy/broken)
191 _rec_tag_symlink_target = 3
192 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
193 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
194 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
195 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
196 _rec_tag_hardlink_target = 8 # hard link target path
197 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
198 _rec_tag_common_v3 = 10 # adds optional size to v2
200 _warned_about_attr_einval = None
203 class ApplyError(Exception):
204 # Thrown when unable to apply any given bit of metadata to a path.
209 # Metadata is stored as a sequence of tagged binary records. Each
210 # record will have some subset of add, encode, load, create, and
211 # apply methods, i.e. _add_foo...
213 # We do allow an "empty" object as a special case, i.e. no
214 # records. One can be created by trying to write Metadata(), and
215 # for such an object, read() will return None. This is used by
216 # "bup save", for example, as a placeholder in cases where
219 # NOTE: if any relevant fields are added or removed, be sure to
220 # update same_file() below.
224 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
225 # must be non-negative and < 10**9.
227 def _add_common(self, path, st):
228 assert(st.st_uid >= 0)
229 assert(st.st_gid >= 0)
230 self.size = st.st_size
233 self.atime = st.st_atime
234 self.mtime = st.st_mtime
235 self.ctime = st.st_ctime
236 self.user = self.group = b''
237 entry = pwd_from_uid(st.st_uid)
239 self.user = entry.pw_name
240 entry = grp_from_gid(st.st_gid)
242 self.group = entry.gr_name
243 self.mode = st.st_mode
244 # Only collect st_rdev if we might need it for a mknod()
245 # during restore. On some platforms (i.e. kFreeBSD), it isn't
246 # stable for other file types. For example "cp -a" will
247 # change it for a plain file.
248 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
249 self.rdev = st.st_rdev
253 def _same_common(self, other):
254 """Return true or false to indicate similarity in the hardlink sense."""
255 return self.uid == other.uid \
256 and self.gid == other.gid \
257 and self.rdev == other.rdev \
258 and self.mtime == other.mtime \
259 and self.ctime == other.ctime \
260 and self.user == other.user \
261 and self.group == other.group \
262 and self.size == other.size
264 def _encode_common(self):
267 atime = xstat.nsecs_to_timespec(self.atime)
268 mtime = xstat.nsecs_to_timespec(self.mtime)
269 ctime = xstat.nsecs_to_timespec(self.ctime)
270 result = vint.pack('vvsvsvvVvVvVv',
283 self.size if self.size is not None else -1)
286 def _load_common_rec(self, port, version=3):
288 # Added trailing size to v2, negative when None.
289 unpack_fmt = 'vvsvsvvVvVvVv'
291 unpack_fmt = 'vvsvsvvVvVvV'
293 unpack_fmt = 'VVsVsVvVvVvV'
295 raise Exception('unexpected common_rec version %d' % version)
296 data = vint.read_bvec(port)
297 values = vint.unpack(unpack_fmt, data)
299 (self.mode, self.uid, self.user, self.gid, self.group,
301 self.atime, atime_ns,
302 self.mtime, mtime_ns,
303 self.ctime, ctime_ns, size) = values
307 (self.mode, self.uid, self.user, self.gid, self.group,
309 self.atime, atime_ns,
310 self.mtime, mtime_ns,
311 self.ctime, ctime_ns) = values
312 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
313 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
314 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
316 def _recognized_file_type(self):
317 return stat.S_ISREG(self.mode) \
318 or stat.S_ISDIR(self.mode) \
319 or stat.S_ISCHR(self.mode) \
320 or stat.S_ISBLK(self.mode) \
321 or stat.S_ISFIFO(self.mode) \
322 or stat.S_ISSOCK(self.mode) \
323 or stat.S_ISLNK(self.mode)
325 def _create_via_common_rec(self, path, create_symlinks=True):
327 raise ApplyError('no metadata - cannot create path '
330 # If the path already exists and is a dir, try rmdir.
331 # If the path already exists and is anything else, try unlink.
334 st = xstat.lstat(path)
336 if e.errno != errno.ENOENT:
339 if stat.S_ISDIR(st.st_mode):
343 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
344 raise Exception('refusing to overwrite non-empty dir '
350 if stat.S_ISREG(self.mode):
351 assert(self._recognized_file_type())
352 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
354 elif stat.S_ISDIR(self.mode):
355 assert(self._recognized_file_type())
356 os.mkdir(path, 0o700)
357 elif stat.S_ISCHR(self.mode):
358 assert(self._recognized_file_type())
359 os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
360 elif stat.S_ISBLK(self.mode):
361 assert(self._recognized_file_type())
362 os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
363 elif stat.S_ISFIFO(self.mode):
364 assert(self._recognized_file_type())
365 os.mkfifo(path, 0o600 | stat.S_IFIFO)
366 elif stat.S_ISSOCK(self.mode):
368 os.mknod(path, 0o600 | stat.S_IFSOCK)
370 if e.errno in (errno.EINVAL, errno.EPERM):
371 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
375 elif stat.S_ISLNK(self.mode):
376 assert(self._recognized_file_type())
377 if self.symlink_target and create_symlinks:
378 # on MacOS, symlink() permissions depend on umask, and there's
379 # no way to chown a symlink after creating it, so we have to
381 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
383 os.symlink(self.symlink_target, path)
386 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
388 assert(not self._recognized_file_type())
389 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
390 % (path_msg(path), self.mode))
392 def _apply_common_rec(self, path, restore_numeric_ids=False):
394 raise ApplyError('no metadata - cannot apply to ' + path_msg(path))
396 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
397 # EACCES errors at this stage are fatal for the current path.
398 if lutime and stat.S_ISLNK(self.mode):
400 lutime(path, (self.atime, self.mtime))
402 if e.errno == errno.EACCES:
403 raise ApplyError('lutime: %s' % e)
408 utime(path, (self.atime, self.mtime))
410 if e.errno == errno.EACCES:
411 raise ApplyError('utime: %s' % e)
415 uid = gid = -1 # By default, do nothing.
417 if self.uid is not None:
419 if self.gid is not None:
421 if not restore_numeric_ids:
422 if self.uid != 0 and self.user:
423 entry = pwd_from_name(self.user)
426 if self.gid != 0 and self.group:
427 entry = grp_from_name(self.group)
430 else: # not superuser - only consider changing the group/gid
431 user_gids = os.getgroups()
432 if self.gid in user_gids:
434 if not restore_numeric_ids and self.gid != 0:
435 # The grp might not exist on the local system.
436 grps = filter(None, [grp_from_gid(x) for x in user_gids])
437 if self.group in [x.gr_name for x in grps]:
438 g = grp_from_name(self.group)
442 if uid != -1 or gid != -1:
444 os.lchown(path, uid, gid)
446 if e.errno == errno.EPERM:
447 add_error('lchown: %s' % e)
448 elif sys.platform.startswith('cygwin') \
449 and e.errno == errno.EINVAL:
450 add_error('lchown: unknown uid/gid (%d/%d) for %s'
451 % (uid, gid, path_msg(path)))
457 os.lchmod(path, stat.S_IMODE(self.mode))
458 except errno.ENOSYS: # Function not implemented
460 elif not stat.S_ISLNK(self.mode):
461 os.chmod(path, stat.S_IMODE(self.mode))
466 def _encode_path(self):
468 return vint.pack('s', self.path)
472 def _load_path_rec(self, port):
473 self.path = vint.unpack('s', vint.read_bvec(port))[0]
478 def _add_symlink_target(self, path, st):
480 if stat.S_ISLNK(st.st_mode):
481 self.symlink_target = os.readlink(path)
483 add_error('readlink: %s' % e)
485 def _encode_symlink_target(self):
486 return self.symlink_target
488 def _load_symlink_target_rec(self, port):
489 target = vint.read_bvec(port)
490 self.symlink_target = target
491 if self.size is None:
492 self.size = len(target)
494 assert(self.size == len(target))
499 def _add_hardlink_target(self, target):
500 self.hardlink_target = target
502 def _same_hardlink_target(self, other):
503 """Return true or false to indicate similarity in the hardlink sense."""
504 return self.hardlink_target == other.hardlink_target
506 def _encode_hardlink_target(self):
507 return self.hardlink_target
509 def _load_hardlink_target_rec(self, port):
510 self.hardlink_target = vint.read_bvec(port)
513 ## POSIX1e ACL records
515 # Recorded as a list:
516 # [txt_id_acl, num_id_acl]
517 # or, if a directory:
518 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
519 # The numeric/text distinction only matters when reading/restoring
521 def _add_posix1e_acl(self, path, st):
524 if not stat.S_ISLNK(st.st_mode):
525 isdir = 1 if stat.S_ISDIR(st.st_mode) else 0
526 self.posix1e_acl = read_acl(path, isdir)
528 def _same_posix1e_acl(self, other):
529 """Return true or false to indicate similarity in the hardlink sense."""
530 return self.posix1e_acl == other.posix1e_acl
532 def _encode_posix1e_acl(self):
533 # Encode as two strings (w/default ACL string possibly empty).
535 acls = self.posix1e_acl
537 return vint.pack('ssss', acls[0], acls[1], b'', b'')
538 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
542 def _load_posix1e_acl_rec(self, port):
543 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
544 if acl_rep[2] == b'':
545 acl_rep = acl_rep[:2]
546 self.posix1e_acl = acl_rep
548 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
549 if not self.posix1e_acl:
553 add_error("%s: can't restore ACLs; posix1e support missing.\n"
558 acls = self.posix1e_acl
559 offs = 1 if restore_numeric_ids else 0
561 apply_acl(path, acls[offs], acls[offs + 2])
563 apply_acl(path, acls[offs])
565 if e.errno == errno.EINVAL:
566 # libacl returns with errno set to EINVAL if a user
567 # (or group) doesn't exist
568 raise ApplyError("POSIX1e ACL: can't create %r for %r"
569 % (acls, path_msg(path)))
570 elif e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
571 raise ApplyError('POSIX1e ACL applyto: %s' % e)
576 ## Linux attributes (lsattr(1), chattr(1))
578 def _add_linux_attr(self, path, st):
579 check_linux_file_attr_api()
580 if not get_linux_file_attr: return
581 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
583 attr = get_linux_file_attr(path)
585 self.linux_attr = attr
587 if e.errno == errno.EACCES:
588 add_error('read Linux attr: %s' % e)
589 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
590 # Assume filesystem doesn't support attrs.
592 elif e.errno == EINVAL:
593 global _warned_about_attr_einval
594 if not _warned_about_attr_einval:
595 log("Ignoring attr EINVAL;"
596 + " if you're not using ntfs-3g, please report: "
597 + path_msg(path) + '\n')
598 _warned_about_attr_einval = True
603 def _same_linux_attr(self, other):
604 """Return true or false to indicate similarity in the hardlink sense."""
605 return self.linux_attr == other.linux_attr
607 def _encode_linux_attr(self):
609 return vint.pack('V', self.linux_attr)
613 def _load_linux_attr_rec(self, port):
614 data = vint.read_bvec(port)
615 self.linux_attr = vint.unpack('V', data)[0]
617 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
619 check_linux_file_attr_api()
620 if not set_linux_file_attr:
621 add_error("%s: can't restore linuxattrs: "
622 "linuxattr support missing.\n" % path_msg(path))
625 set_linux_file_attr(path, self.linux_attr)
627 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
628 raise ApplyError('Linux chattr: %s (0x%s)'
629 % (e, hex(self.linux_attr)))
630 elif e.errno == EINVAL:
631 msg = "if you're not using ntfs-3g, please report"
632 raise ApplyError('Linux chattr: %s (0x%s) (%s)'
633 % (e, hex(self.linux_attr), msg))
638 ## Linux extended attributes (getfattr(1), setfattr(1))
640 def _add_linux_xattr(self, path, st):
643 self.linux_xattr = xattr.get_all(path, nofollow=True)
644 except EnvironmentError as e:
645 if e.errno != errno.EOPNOTSUPP:
648 def _same_linux_xattr(self, other):
649 """Return true or false to indicate similarity in the hardlink sense."""
650 return self.linux_xattr == other.linux_xattr
652 def _encode_linux_xattr(self):
654 result = vint.pack('V', len(self.linux_xattr))
655 for name, value in self.linux_xattr:
656 result += vint.pack('ss', name, value)
661 def _load_linux_xattr_rec(self, file):
662 data = vint.read_bvec(file)
663 memfile = BytesIO(data)
665 for i in range(vint.read_vuint(memfile)):
666 key = vint.read_bvec(memfile)
667 value = vint.read_bvec(memfile)
668 result.append((key, value))
669 self.linux_xattr = result
671 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
674 add_error("%s: can't restore xattr; xattr support missing.\n"
677 if not self.linux_xattr:
680 existing_xattrs = set(xattr.list(path, nofollow=True))
682 if e.errno == errno.EACCES:
683 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
686 for k, v in self.linux_xattr:
687 if k not in existing_xattrs \
688 or v != xattr.get(path, k, nofollow=True):
690 xattr.set(path, k, v, nofollow=True)
692 if e.errno == errno.EPERM \
693 or e.errno == errno.EOPNOTSUPP:
694 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
697 existing_xattrs -= frozenset([k])
698 for k in existing_xattrs:
700 xattr.remove(path, k, nofollow=True)
702 if e.errno in (errno.EPERM, errno.EACCES):
703 raise ApplyError('xattr.remove %r: %s' % (path_msg(path), e))
708 self.mode = self.uid = self.gid = self.user = self.group = None
709 self.atime = self.mtime = self.ctime = None
713 self.symlink_target = None
714 self.hardlink_target = None
715 self.linux_attr = None
716 self.linux_xattr = None
717 self.posix1e_acl = None
719 def __eq__(self, other):
720 if not isinstance(other, Metadata): return False
721 if self.mode != other.mode: return False
722 if self.mtime != other.mtime: return False
723 if self.ctime != other.ctime: return False
724 if self.atime != other.atime: return False
725 if self.path != other.path: return False
726 if self.uid != other.uid: return False
727 if self.gid != other.gid: return False
728 if self.size != other.size: return False
729 if self.user != other.user: return False
730 if self.group != other.group: return False
731 if self.symlink_target != other.symlink_target: return False
732 if self.hardlink_target != other.hardlink_target: return False
733 if self.linux_attr != other.linux_attr: return False
734 if self.posix1e_acl != other.posix1e_acl: return False
737 def __ne__(self, other):
738 return not self.__eq__(other)
741 return hash((self.mode,
752 self.hardlink_target,
757 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
758 if self.path is not None:
759 result += ' path:' + repr(self.path)
760 if self.mode is not None:
761 result += ' mode: %o (%s)' % (self.mode, xstat.mode_str(self.mode))
762 if self.uid is not None:
763 result += ' uid:' + str(self.uid)
764 if self.gid is not None:
765 result += ' gid:' + str(self.gid)
766 if self.user is not None:
767 result += ' user:' + repr(self.user)
768 if self.group is not None:
769 result += ' group:' + repr(self.group)
770 if self.size is not None:
771 result += ' size:' + repr(self.size)
772 for name, val in (('atime', self.atime),
773 ('mtime', self.mtime),
774 ('ctime', self.ctime)):
776 result += ' %s:%r (%d)' \
778 strftime('%Y-%m-%d %H:%M %z',
779 gmtime(xstat.fstime_floor_secs(val))),
782 return ''.join(result)
784 def write(self, port, include_path=True):
785 port.write(self.encode(include_path=include_path))
787 def encode(self, include_path=True):
789 records = include_path and [(_rec_tag_path, self._encode_path())] or []
790 records.extend([(_rec_tag_common_v3, self._encode_common()),
791 (_rec_tag_symlink_target,
792 self._encode_symlink_target()),
793 (_rec_tag_hardlink_target,
794 self._encode_hardlink_target()),
795 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
796 (_rec_tag_linux_attr, self._encode_linux_attr()),
797 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
798 for tag, data in records:
800 ret.extend((vint.encode_vuint(tag),
801 vint.encode_bvec(data)))
802 ret.append(vint.encode_vuint(_rec_tag_end))
806 return deepcopy(self)
810 # This method should either return a valid Metadata object,
811 # return None if there was no information at all (just a
812 # _rec_tag_end), throw EOFError if there was nothing at all to
813 # read, or throw an Exception if a valid object could not be
815 tag = vint.read_vuint(port)
816 if tag == _rec_tag_end:
818 try: # From here on, EOF is an error.
820 while True: # only exit is error (exception) or _rec_tag_end
821 if tag == _rec_tag_path:
822 result._load_path_rec(port)
823 elif tag == _rec_tag_common_v3:
824 result._load_common_rec(port, version=3)
825 elif tag == _rec_tag_common_v2:
826 result._load_common_rec(port, version=2)
827 elif tag == _rec_tag_symlink_target:
828 result._load_symlink_target_rec(port)
829 elif tag == _rec_tag_hardlink_target:
830 result._load_hardlink_target_rec(port)
831 elif tag == _rec_tag_posix1e_acl:
832 result._load_posix1e_acl_rec(port)
833 elif tag == _rec_tag_linux_attr:
834 result._load_linux_attr_rec(port)
835 elif tag == _rec_tag_linux_xattr:
836 result._load_linux_xattr_rec(port)
837 elif tag == _rec_tag_end:
839 elif tag == _rec_tag_common_v1: # Should be very rare.
840 result._load_common_rec(port, version=1)
841 else: # unknown record
843 tag = vint.read_vuint(port)
845 raise Exception("EOF while reading Metadata")
848 return stat.S_ISDIR(self.mode)
850 def create_path(self, path, create_symlinks=True):
851 self._create_via_common_rec(path, create_symlinks=create_symlinks)
853 def apply_to_path(self, path=None, restore_numeric_ids=False):
854 # apply metadata to path -- file must exist
858 raise Exception('Metadata.apply_to_path() called with no path')
859 if not self._recognized_file_type():
860 add_error('not applying metadata to "%s"' % path_msg(path)
861 + ' with unrecognized mode "0x%x"\n' % self.mode)
863 num_ids = restore_numeric_ids
864 for apply_metadata in (self._apply_common_rec,
865 self._apply_posix1e_acl_rec,
866 self._apply_linux_attr_rec,
867 self._apply_linux_xattr_rec):
869 apply_metadata(path, restore_numeric_ids=num_ids)
870 except ApplyError as e:
873 def same_file(self, other):
874 """Compare this to other for equivalency. Return true if
875 their information implies they could represent the same file
876 on disk, in the hardlink sense. Assume they're both regular
878 return self._same_common(other) \
879 and self._same_hardlink_target(other) \
880 and self._same_posix1e_acl(other) \
881 and self._same_linux_attr(other) \
882 and self._same_linux_xattr(other)
885 def from_path(path, statinfo=None, archive_path=None,
886 save_symlinks=True, hardlink_target=None,
888 # This function is also a test hook; see test-save-errors
889 """Return the metadata associated with the path. When normalized is
890 true, return the metadata appropriate for a typical save, which
891 may or may not be all of it."""
893 result.path = archive_path
894 st = statinfo or xstat.lstat(path)
895 result._add_common(path, st)
897 result._add_symlink_target(path, st)
898 result._add_hardlink_target(hardlink_target)
899 result._add_posix1e_acl(path, st)
900 result._add_linux_attr(path, st)
901 result._add_linux_xattr(path, st)
903 # Only store sizes for regular files and symlinks for now.
904 if not (stat.S_ISREG(result.mode) or stat.S_ISLNK(result.mode)):
909 def save_tree(output_file, paths,
915 # Issue top-level rewrite warnings.
917 safe_path = _clean_up_path_for_archive(path)
918 if safe_path != path:
919 log('archiving "%s" as "%s"\n'
920 % (path_msg(path), path_msg(safe_path)))
924 safe_path = _clean_up_path_for_archive(p)
926 if stat.S_ISDIR(st.st_mode):
928 m = from_path(p, statinfo=st, archive_path=safe_path,
929 save_symlinks=save_symlinks)
931 print(m.path, file=sys.stderr)
932 m.write(output_file, include_path=write_paths)
934 start_dir = os.getcwd()
936 for (p, st) in recursive_dirlist(paths, xdev=xdev):
937 dirlist_dir = os.getcwd()
939 safe_path = _clean_up_path_for_archive(p)
940 m = from_path(p, statinfo=st, archive_path=safe_path,
941 save_symlinks=save_symlinks)
943 print(m.path, file=sys.stderr)
944 m.write(output_file, include_path=write_paths)
945 os.chdir(dirlist_dir)
950 def _set_up_path(meta, create_symlinks=True):
951 # Allow directories to exist as a special case -- might have
952 # been created by an earlier longer path.
956 parent = os.path.dirname(meta.path)
959 meta.create_path(meta.path, create_symlinks=create_symlinks)
962 all_fields = frozenset(['path',
979 def summary_bytes(meta, numeric_ids = False, classification = None,
980 human_readable = False):
981 """Return bytes containing the "ls -l" style listing for meta.
982 Classification may be "all", "type", or None."""
983 user_str = group_str = size_or_dev_str = b'?'
984 symlink_target = None
987 mode_str = xstat.mode_str(meta.mode).encode('ascii')
988 symlink_target = meta.symlink_target
989 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
990 mtime_str = strftime('%Y-%m-%d %H:%M',
991 time.localtime(mtime_secs)).encode('ascii')
992 if meta.user and not numeric_ids:
994 elif meta.uid != None:
995 user_str = str(meta.uid).encode()
996 if meta.group and not numeric_ids:
997 group_str = meta.group
998 elif meta.gid != None:
999 group_str = str(meta.gid).encode()
1000 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
1002 size_or_dev_str = ('%d,%d' % (os.major(meta.rdev),
1003 os.minor(meta.rdev))).encode()
1004 elif meta.size != None:
1006 size_or_dev_str = format_filesize(meta.size).encode()
1008 size_or_dev_str = str(meta.size).encode()
1010 size_or_dev_str = b'-'
1012 classification_str = \
1013 xstat.classification_str(meta.mode,
1014 classification == 'all').encode()
1016 mode_str = b'?' * 10
1017 mtime_str = b'????-??-?? ??:??'
1018 classification_str = b'?'
1022 name += classification_str
1024 name += b' -> ' + meta.symlink_target
1026 return b'%-10s %-11s %11s %16s %s' % (mode_str,
1027 user_str + b'/' + group_str,
1033 def detailed_bytes(meta, fields = None):
1034 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
1035 # 0", "link-target:", etc.
1040 if 'path' in fields:
1041 path = meta.path or b''
1042 result.append(b'path: ' + path)
1043 if 'mode' in fields:
1044 result.append(b'mode: %o (%s)'
1045 % (meta.mode, xstat.mode_str(meta.mode).encode('ascii')))
1046 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1047 result.append(b'link-target: ' + meta.symlink_target)
1048 if 'rdev' in fields:
1050 result.append(b'rdev: %d,%d' % (os.major(meta.rdev),
1051 os.minor(meta.rdev)))
1053 result.append(b'rdev: 0')
1054 if 'size' in fields and meta.size is not None:
1055 result.append(b'size: %d' % meta.size)
1057 result.append(b'uid: %d' % meta.uid)
1059 result.append(b'gid: %d' % meta.gid)
1060 if 'user' in fields:
1061 result.append(b'user: ' + meta.user)
1062 if 'group' in fields:
1063 result.append(b'group: ' + meta.group)
1064 if 'atime' in fields:
1065 # If we don't have xstat.lutime, that means we have to use
1066 # utime(), and utime() has no way to set the mtime/atime of a
1067 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1068 # so let's not report it. (That way scripts comparing
1069 # before/after won't trigger.)
1070 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1071 result.append(b'atime: ' + xstat.fstime_to_sec_bytes(meta.atime))
1073 result.append(b'atime: 0')
1074 if 'mtime' in fields:
1075 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1076 result.append(b'mtime: ' + xstat.fstime_to_sec_bytes(meta.mtime))
1078 result.append(b'mtime: 0')
1079 if 'ctime' in fields:
1080 result.append(b'ctime: ' + xstat.fstime_to_sec_bytes(meta.ctime))
1081 if 'linux-attr' in fields and meta.linux_attr:
1082 result.append(b'linux-attr: %x' % meta.linux_attr)
1083 if 'linux-xattr' in fields and meta.linux_xattr:
1084 for name, value in meta.linux_xattr:
1085 result.append(b'linux-xattr: %s -> %s' % (name, value))
1086 if 'posix1e-acl' in fields and meta.posix1e_acl:
1087 acl = meta.posix1e_acl[0]
1088 result.append(b'posix1e-acl: ' + acl + b'\n')
1089 if stat.S_ISDIR(meta.mode):
1090 def_acl = meta.posix1e_acl[2]
1091 result.append(b'posix1e-acl-default: ' + def_acl + b'\n')
1092 return b'\n'.join(result)
1095 class _ArchiveIterator:
1098 return Metadata.read(self._file)
1100 raise StopIteration()
1107 def __init__(self, file):
1111 def display_archive(file, out):
1114 for meta in _ArchiveIterator(file):
1117 out.write(detailed_bytes(meta))
1121 for meta in _ArchiveIterator(file):
1122 out.write(summary_bytes(meta))
1125 for meta in _ArchiveIterator(file):
1127 log('bup: no metadata path, but asked to only display path'
1128 ' (increase verbosity?)')
1130 out.write(meta.path)
1134 def start_extract(file, create_symlinks=True):
1135 for meta in _ArchiveIterator(file):
1136 if not meta: # Hit end record.
1139 print(path_msg(meta.path), file=sys.stderr)
1140 xpath = _clean_up_extract_path(meta.path)
1142 add_error(Exception('skipping risky path "%s"'
1143 % path_msg(meta.path)))
1146 _set_up_path(meta, create_symlinks=create_symlinks)
1149 def finish_extract(file, restore_numeric_ids=False):
1151 for meta in _ArchiveIterator(file):
1152 if not meta: # Hit end record.
1154 xpath = _clean_up_extract_path(meta.path)
1156 add_error(Exception('skipping risky path "%s"'
1157 % path_msg(meta.path)))
1159 if os.path.isdir(meta.path):
1160 all_dirs.append(meta)
1163 print(path_msg(meta.path), file=sys.stderr)
1164 meta.apply_to_path(path=xpath,
1165 restore_numeric_ids=restore_numeric_ids)
1166 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1167 for dir in all_dirs:
1168 # Don't need to check xpath -- won't be in all_dirs if not OK.
1169 xpath = _clean_up_extract_path(dir.path)
1171 print(path_msg(dir.path), file=sys.stderr)
1172 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1175 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1176 # For now, just store all the directories and handle them last,
1179 for meta in _ArchiveIterator(file):
1180 if not meta: # Hit end record.
1182 xpath = _clean_up_extract_path(meta.path)
1184 add_error(Exception('skipping risky path "%s"'
1185 % path_msg(meta.path)))
1189 print('+', path_msg(meta.path), file=sys.stderr)
1190 _set_up_path(meta, create_symlinks=create_symlinks)
1191 if os.path.isdir(meta.path):
1192 all_dirs.append(meta)
1195 print('=', path_msg(meta.path), file=sys.stderr)
1196 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1197 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1198 for dir in all_dirs:
1199 # Don't need to check xpath -- won't be in all_dirs if not OK.
1200 xpath = _clean_up_extract_path(dir.path)
1202 print('=', path_msg(xpath), file=sys.stderr)
1203 # Shouldn't have to check for risky paths here (omitted above).
1204 dir.apply_to_path(path=dir.path,
1205 restore_numeric_ids=restore_numeric_ids)