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 records = include_path and [(_rec_tag_path, self._encode_path())] or []
786 records.extend([(_rec_tag_common_v3, self._encode_common()),
787 (_rec_tag_symlink_target,
788 self._encode_symlink_target()),
789 (_rec_tag_hardlink_target,
790 self._encode_hardlink_target()),
791 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
792 (_rec_tag_linux_attr, self._encode_linux_attr()),
793 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
794 for tag, data in records:
796 vint.write_vuint(port, tag)
797 vint.write_bvec(port, data)
798 vint.write_vuint(port, _rec_tag_end)
800 def encode(self, include_path=True):
802 self.write(port, include_path)
803 return port.getvalue()
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 """Return the metadata associated with the path. When normalized is
889 true, return the metadata appropriate for a typical save, which
890 may or may not be all of it."""
892 result.path = archive_path
893 st = statinfo or xstat.lstat(path)
894 result._add_common(path, st)
896 result._add_symlink_target(path, st)
897 result._add_hardlink_target(hardlink_target)
898 result._add_posix1e_acl(path, st)
899 result._add_linux_attr(path, st)
900 result._add_linux_xattr(path, st)
902 # Only store sizes for regular files and symlinks for now.
903 if not (stat.S_ISREG(result.mode) or stat.S_ISLNK(result.mode)):
908 def save_tree(output_file, paths,
914 # Issue top-level rewrite warnings.
916 safe_path = _clean_up_path_for_archive(path)
917 if safe_path != path:
918 log('archiving "%s" as "%s"\n'
919 % (path_msg(path), path_msg(safe_path)))
923 safe_path = _clean_up_path_for_archive(p)
925 if stat.S_ISDIR(st.st_mode):
927 m = from_path(p, statinfo=st, archive_path=safe_path,
928 save_symlinks=save_symlinks)
930 print(m.path, file=sys.stderr)
931 m.write(output_file, include_path=write_paths)
933 start_dir = os.getcwd()
935 for (p, st) in recursive_dirlist(paths, xdev=xdev):
936 dirlist_dir = os.getcwd()
938 safe_path = _clean_up_path_for_archive(p)
939 m = from_path(p, statinfo=st, archive_path=safe_path,
940 save_symlinks=save_symlinks)
942 print(m.path, file=sys.stderr)
943 m.write(output_file, include_path=write_paths)
944 os.chdir(dirlist_dir)
949 def _set_up_path(meta, create_symlinks=True):
950 # Allow directories to exist as a special case -- might have
951 # been created by an earlier longer path.
955 parent = os.path.dirname(meta.path)
958 meta.create_path(meta.path, create_symlinks=create_symlinks)
961 all_fields = frozenset(['path',
978 def summary_bytes(meta, numeric_ids = False, classification = None,
979 human_readable = False):
980 """Return bytes containing the "ls -l" style listing for meta.
981 Classification may be "all", "type", or None."""
982 user_str = group_str = size_or_dev_str = b'?'
983 symlink_target = None
986 mode_str = xstat.mode_str(meta.mode).encode('ascii')
987 symlink_target = meta.symlink_target
988 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
989 mtime_str = strftime('%Y-%m-%d %H:%M',
990 time.localtime(mtime_secs)).encode('ascii')
991 if meta.user and not numeric_ids:
993 elif meta.uid != None:
994 user_str = str(meta.uid).encode()
995 if meta.group and not numeric_ids:
996 group_str = meta.group
997 elif meta.gid != None:
998 group_str = str(meta.gid).encode()
999 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
1001 size_or_dev_str = ('%d,%d' % (os.major(meta.rdev),
1002 os.minor(meta.rdev))).encode()
1003 elif meta.size != None:
1005 size_or_dev_str = format_filesize(meta.size).encode()
1007 size_or_dev_str = str(meta.size).encode()
1009 size_or_dev_str = b'-'
1011 classification_str = \
1012 xstat.classification_str(meta.mode,
1013 classification == 'all').encode()
1015 mode_str = b'?' * 10
1016 mtime_str = b'????-??-?? ??:??'
1017 classification_str = b'?'
1021 name += classification_str
1023 name += b' -> ' + meta.symlink_target
1025 return b'%-10s %-11s %11s %16s %s' % (mode_str,
1026 user_str + b'/' + group_str,
1032 def detailed_bytes(meta, fields = None):
1033 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
1034 # 0", "link-target:", etc.
1039 if 'path' in fields:
1040 path = meta.path or b''
1041 result.append(b'path: ' + path)
1042 if 'mode' in fields:
1043 result.append(b'mode: %o (%s)'
1044 % (meta.mode, xstat.mode_str(meta.mode).encode('ascii')))
1045 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1046 result.append(b'link-target: ' + meta.symlink_target)
1047 if 'rdev' in fields:
1049 result.append(b'rdev: %d,%d' % (os.major(meta.rdev),
1050 os.minor(meta.rdev)))
1052 result.append(b'rdev: 0')
1053 if 'size' in fields and meta.size is not None:
1054 result.append(b'size: %d' % meta.size)
1056 result.append(b'uid: %d' % meta.uid)
1058 result.append(b'gid: %d' % meta.gid)
1059 if 'user' in fields:
1060 result.append(b'user: ' + meta.user)
1061 if 'group' in fields:
1062 result.append(b'group: ' + meta.group)
1063 if 'atime' in fields:
1064 # If we don't have xstat.lutime, that means we have to use
1065 # utime(), and utime() has no way to set the mtime/atime of a
1066 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1067 # so let's not report it. (That way scripts comparing
1068 # before/after won't trigger.)
1069 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1070 result.append(b'atime: ' + xstat.fstime_to_sec_bytes(meta.atime))
1072 result.append(b'atime: 0')
1073 if 'mtime' in fields:
1074 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1075 result.append(b'mtime: ' + xstat.fstime_to_sec_bytes(meta.mtime))
1077 result.append(b'mtime: 0')
1078 if 'ctime' in fields:
1079 result.append(b'ctime: ' + xstat.fstime_to_sec_bytes(meta.ctime))
1080 if 'linux-attr' in fields and meta.linux_attr:
1081 result.append(b'linux-attr: %x' % meta.linux_attr)
1082 if 'linux-xattr' in fields and meta.linux_xattr:
1083 for name, value in meta.linux_xattr:
1084 result.append(b'linux-xattr: %s -> %s' % (name, value))
1085 if 'posix1e-acl' in fields and meta.posix1e_acl:
1086 acl = meta.posix1e_acl[0]
1087 result.append(b'posix1e-acl: ' + acl + b'\n')
1088 if stat.S_ISDIR(meta.mode):
1089 def_acl = meta.posix1e_acl[2]
1090 result.append(b'posix1e-acl-default: ' + def_acl + b'\n')
1091 return b'\n'.join(result)
1094 class _ArchiveIterator:
1097 return Metadata.read(self._file)
1099 raise StopIteration()
1106 def __init__(self, file):
1110 def display_archive(file, out):
1113 for meta in _ArchiveIterator(file):
1116 out.write(detailed_bytes(meta))
1120 for meta in _ArchiveIterator(file):
1121 out.write(summary_bytes(meta))
1124 for meta in _ArchiveIterator(file):
1126 log('bup: no metadata path, but asked to only display path'
1127 ' (increase verbosity?)')
1129 out.write(meta.path)
1133 def start_extract(file, create_symlinks=True):
1134 for meta in _ArchiveIterator(file):
1135 if not meta: # Hit end record.
1138 print(path_msg(meta.path), file=sys.stderr)
1139 xpath = _clean_up_extract_path(meta.path)
1141 add_error(Exception('skipping risky path "%s"'
1142 % path_msg(meta.path)))
1145 _set_up_path(meta, create_symlinks=create_symlinks)
1148 def finish_extract(file, restore_numeric_ids=False):
1150 for meta in _ArchiveIterator(file):
1151 if not meta: # Hit end record.
1153 xpath = _clean_up_extract_path(meta.path)
1155 add_error(Exception('skipping risky path "%s"'
1156 % path_msg(meta.path)))
1158 if os.path.isdir(meta.path):
1159 all_dirs.append(meta)
1162 print(path_msg(meta.path), file=sys.stderr)
1163 meta.apply_to_path(path=xpath,
1164 restore_numeric_ids=restore_numeric_ids)
1165 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1166 for dir in all_dirs:
1167 # Don't need to check xpath -- won't be in all_dirs if not OK.
1168 xpath = _clean_up_extract_path(dir.path)
1170 print(path_msg(dir.path), file=sys.stderr)
1171 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1174 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1175 # For now, just store all the directories and handle them last,
1178 for meta in _ArchiveIterator(file):
1179 if not meta: # Hit end record.
1181 xpath = _clean_up_extract_path(meta.path)
1183 add_error(Exception('skipping risky path "%s"'
1184 % path_msg(meta.path)))
1188 print('+', path_msg(meta.path), file=sys.stderr)
1189 _set_up_path(meta, create_symlinks=create_symlinks)
1190 if os.path.isdir(meta.path):
1191 all_dirs.append(meta)
1194 print('=', path_msg(meta.path), file=sys.stderr)
1195 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1196 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1197 for dir in all_dirs:
1198 # Don't need to check xpath -- won't be in all_dirs if not OK.
1199 xpath = _clean_up_extract_path(dir.path)
1201 print('=', path_msg(xpath), file=sys.stderr)
1202 # Shouldn't have to check for risky paths here (omitted above).
1203 dir.apply_to_path(path=dir.path,
1204 restore_numeric_ids=restore_numeric_ids)