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)
482 # might have read a different link than the
483 # one that was in place when we did stat()
484 self.size = len(self.symlink_target)
486 add_error('readlink: %s' % e)
488 def _encode_symlink_target(self):
489 return self.symlink_target
491 def _load_symlink_target_rec(self, port):
492 target = vint.read_bvec(port)
493 self.symlink_target = target
494 if self.size is None:
495 self.size = len(target)
497 assert(self.size == len(target))
502 def _add_hardlink_target(self, target):
503 self.hardlink_target = target
505 def _same_hardlink_target(self, other):
506 """Return true or false to indicate similarity in the hardlink sense."""
507 return self.hardlink_target == other.hardlink_target
509 def _encode_hardlink_target(self):
510 return self.hardlink_target
512 def _load_hardlink_target_rec(self, port):
513 self.hardlink_target = vint.read_bvec(port)
516 ## POSIX1e ACL records
518 # Recorded as a list:
519 # [txt_id_acl, num_id_acl]
520 # or, if a directory:
521 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
522 # The numeric/text distinction only matters when reading/restoring
524 def _add_posix1e_acl(self, path, st):
527 if not stat.S_ISLNK(st.st_mode):
528 isdir = 1 if stat.S_ISDIR(st.st_mode) else 0
529 self.posix1e_acl = read_acl(path, isdir)
531 def _same_posix1e_acl(self, other):
532 """Return true or false to indicate similarity in the hardlink sense."""
533 return self.posix1e_acl == other.posix1e_acl
535 def _encode_posix1e_acl(self):
536 # Encode as two strings (w/default ACL string possibly empty).
538 acls = self.posix1e_acl
540 return vint.pack('ssss', acls[0], acls[1], b'', b'')
541 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
545 def _load_posix1e_acl_rec(self, port):
546 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
547 if acl_rep[2] == b'':
548 acl_rep = acl_rep[:2]
549 self.posix1e_acl = acl_rep
551 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
552 if not self.posix1e_acl:
556 add_error("%s: can't restore ACLs; posix1e support missing.\n"
561 acls = self.posix1e_acl
562 offs = 1 if restore_numeric_ids else 0
564 apply_acl(path, acls[offs], acls[offs + 2])
566 apply_acl(path, acls[offs])
568 if e.errno == errno.EINVAL:
569 # libacl returns with errno set to EINVAL if a user
570 # (or group) doesn't exist
571 raise ApplyError("POSIX1e ACL: can't create %r for %r"
572 % (acls, path_msg(path)))
573 elif e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
574 raise ApplyError('POSIX1e ACL applyto: %s' % e)
579 ## Linux attributes (lsattr(1), chattr(1))
581 def _add_linux_attr(self, path, st):
582 check_linux_file_attr_api()
583 if not get_linux_file_attr: return
584 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
586 attr = get_linux_file_attr(path)
588 self.linux_attr = attr
590 if e.errno == errno.EACCES:
591 add_error('read Linux attr: %s' % e)
592 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
593 # Assume filesystem doesn't support attrs.
595 elif e.errno == EINVAL:
596 global _warned_about_attr_einval
597 if not _warned_about_attr_einval:
598 log("Ignoring attr EINVAL;"
599 + " if you're not using ntfs-3g, please report: "
600 + path_msg(path) + '\n')
601 _warned_about_attr_einval = True
606 def _same_linux_attr(self, other):
607 """Return true or false to indicate similarity in the hardlink sense."""
608 return self.linux_attr == other.linux_attr
610 def _encode_linux_attr(self):
612 return vint.pack('V', self.linux_attr)
616 def _load_linux_attr_rec(self, port):
617 data = vint.read_bvec(port)
618 self.linux_attr = vint.unpack('V', data)[0]
620 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
622 check_linux_file_attr_api()
623 if not set_linux_file_attr:
624 add_error("%s: can't restore linuxattrs: "
625 "linuxattr support missing.\n" % path_msg(path))
628 set_linux_file_attr(path, self.linux_attr)
630 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
631 raise ApplyError('Linux chattr: %s (0x%s)'
632 % (e, hex(self.linux_attr)))
633 elif e.errno == EINVAL:
634 msg = "if you're not using ntfs-3g, please report"
635 raise ApplyError('Linux chattr: %s (0x%s) (%s)'
636 % (e, hex(self.linux_attr), msg))
641 ## Linux extended attributes (getfattr(1), setfattr(1))
643 def _add_linux_xattr(self, path, st):
646 self.linux_xattr = xattr.get_all(path, nofollow=True)
647 except EnvironmentError as e:
648 if e.errno != errno.EOPNOTSUPP:
651 def _same_linux_xattr(self, other):
652 """Return true or false to indicate similarity in the hardlink sense."""
653 return self.linux_xattr == other.linux_xattr
655 def _encode_linux_xattr(self):
657 result = vint.pack('V', len(self.linux_xattr))
658 for name, value in self.linux_xattr:
659 result += vint.pack('ss', name, value)
664 def _load_linux_xattr_rec(self, file):
665 data = vint.read_bvec(file)
666 memfile = BytesIO(data)
668 for i in range(vint.read_vuint(memfile)):
669 key = vint.read_bvec(memfile)
670 value = vint.read_bvec(memfile)
671 result.append((key, value))
672 self.linux_xattr = result
674 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
677 add_error("%s: can't restore xattr; xattr support missing.\n"
680 if not self.linux_xattr:
683 existing_xattrs = set(xattr.list(path, nofollow=True))
685 if e.errno == errno.EACCES:
686 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
689 for k, v in self.linux_xattr:
690 if k not in existing_xattrs \
691 or v != xattr.get(path, k, nofollow=True):
693 xattr.set(path, k, v, nofollow=True)
695 if e.errno == errno.EPERM \
696 or e.errno == errno.EOPNOTSUPP:
697 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
700 existing_xattrs -= frozenset([k])
701 for k in existing_xattrs:
703 xattr.remove(path, k, nofollow=True)
705 if e.errno in (errno.EPERM, errno.EACCES):
706 raise ApplyError('xattr.remove %r: %s' % (path_msg(path), e))
711 self.mode = self.uid = self.gid = self.user = self.group = None
712 self.atime = self.mtime = self.ctime = None
716 self.symlink_target = None
717 self.hardlink_target = None
718 self.linux_attr = None
719 self.linux_xattr = None
720 self.posix1e_acl = None
722 def __eq__(self, other):
723 if not isinstance(other, Metadata): return False
724 if self.mode != other.mode: return False
725 if self.mtime != other.mtime: return False
726 if self.ctime != other.ctime: return False
727 if self.atime != other.atime: return False
728 if self.path != other.path: return False
729 if self.uid != other.uid: return False
730 if self.gid != other.gid: return False
731 if self.size != other.size: return False
732 if self.user != other.user: return False
733 if self.group != other.group: return False
734 if self.symlink_target != other.symlink_target: return False
735 if self.hardlink_target != other.hardlink_target: return False
736 if self.linux_attr != other.linux_attr: return False
737 if self.posix1e_acl != other.posix1e_acl: return False
740 def __ne__(self, other):
741 return not self.__eq__(other)
744 return hash((self.mode,
755 self.hardlink_target,
760 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
761 if self.path is not None:
762 result += ' path:' + repr(self.path)
763 if self.mode is not None:
764 result += ' mode: %o (%s)' % (self.mode, xstat.mode_str(self.mode))
765 if self.uid is not None:
766 result += ' uid:' + str(self.uid)
767 if self.gid is not None:
768 result += ' gid:' + str(self.gid)
769 if self.user is not None:
770 result += ' user:' + repr(self.user)
771 if self.group is not None:
772 result += ' group:' + repr(self.group)
773 if self.size is not None:
774 result += ' size:' + repr(self.size)
775 for name, val in (('atime', self.atime),
776 ('mtime', self.mtime),
777 ('ctime', self.ctime)):
779 result += ' %s:%r (%d)' \
781 strftime('%Y-%m-%d %H:%M %z',
782 gmtime(xstat.fstime_floor_secs(val))),
785 return ''.join(result)
787 def write(self, port, include_path=True):
788 port.write(self.encode(include_path=include_path))
790 def encode(self, include_path=True):
792 records = include_path and [(_rec_tag_path, self._encode_path())] or []
793 records.extend([(_rec_tag_common_v3, self._encode_common()),
794 (_rec_tag_symlink_target,
795 self._encode_symlink_target()),
796 (_rec_tag_hardlink_target,
797 self._encode_hardlink_target()),
798 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
799 (_rec_tag_linux_attr, self._encode_linux_attr()),
800 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
801 for tag, data in records:
803 ret.extend((vint.encode_vuint(tag),
804 vint.encode_bvec(data)))
805 ret.append(vint.encode_vuint(_rec_tag_end))
809 return deepcopy(self)
813 # This method should either return a valid Metadata object,
814 # return None if there was no information at all (just a
815 # _rec_tag_end), throw EOFError if there was nothing at all to
816 # read, or throw an Exception if a valid object could not be
818 tag = vint.read_vuint(port)
819 if tag == _rec_tag_end:
821 try: # From here on, EOF is an error.
823 while True: # only exit is error (exception) or _rec_tag_end
824 if tag == _rec_tag_path:
825 result._load_path_rec(port)
826 elif tag == _rec_tag_common_v3:
827 result._load_common_rec(port, version=3)
828 elif tag == _rec_tag_common_v2:
829 result._load_common_rec(port, version=2)
830 elif tag == _rec_tag_symlink_target:
831 result._load_symlink_target_rec(port)
832 elif tag == _rec_tag_hardlink_target:
833 result._load_hardlink_target_rec(port)
834 elif tag == _rec_tag_posix1e_acl:
835 result._load_posix1e_acl_rec(port)
836 elif tag == _rec_tag_linux_attr:
837 result._load_linux_attr_rec(port)
838 elif tag == _rec_tag_linux_xattr:
839 result._load_linux_xattr_rec(port)
840 elif tag == _rec_tag_end:
842 elif tag == _rec_tag_common_v1: # Should be very rare.
843 result._load_common_rec(port, version=1)
844 else: # unknown record
846 tag = vint.read_vuint(port)
848 raise Exception("EOF while reading Metadata")
851 return stat.S_ISDIR(self.mode)
853 def create_path(self, path, create_symlinks=True):
854 self._create_via_common_rec(path, create_symlinks=create_symlinks)
856 def apply_to_path(self, path=None, restore_numeric_ids=False):
857 # apply metadata to path -- file must exist
861 raise Exception('Metadata.apply_to_path() called with no path')
862 if not self._recognized_file_type():
863 add_error('not applying metadata to "%s"' % path_msg(path)
864 + ' with unrecognized mode "0x%x"\n' % self.mode)
866 num_ids = restore_numeric_ids
867 for apply_metadata in (self._apply_common_rec,
868 self._apply_posix1e_acl_rec,
869 self._apply_linux_attr_rec,
870 self._apply_linux_xattr_rec):
872 apply_metadata(path, restore_numeric_ids=num_ids)
873 except ApplyError as e:
876 def same_file(self, other):
877 """Compare this to other for equivalency. Return true if
878 their information implies they could represent the same file
879 on disk, in the hardlink sense. Assume they're both regular
881 return self._same_common(other) \
882 and self._same_hardlink_target(other) \
883 and self._same_posix1e_acl(other) \
884 and self._same_linux_attr(other) \
885 and self._same_linux_xattr(other)
888 def from_path(path, statinfo=None, archive_path=None,
889 save_symlinks=True, hardlink_target=None,
891 # This function is also a test hook; see test-save-errors
892 """Return the metadata associated with the path. When normalized is
893 true, return the metadata appropriate for a typical save, which
894 may or may not be all of it."""
896 result.path = archive_path
897 st = statinfo or xstat.lstat(path)
898 result._add_common(path, st)
900 result._add_symlink_target(path, st)
901 result._add_hardlink_target(hardlink_target)
902 result._add_posix1e_acl(path, st)
903 result._add_linux_attr(path, st)
904 result._add_linux_xattr(path, st)
906 # Only store sizes for regular files and symlinks for now.
907 if not (stat.S_ISREG(result.mode) or stat.S_ISLNK(result.mode)):
912 def save_tree(output_file, paths,
918 # Issue top-level rewrite warnings.
920 safe_path = _clean_up_path_for_archive(path)
921 if safe_path != path:
922 log('archiving "%s" as "%s"\n'
923 % (path_msg(path), path_msg(safe_path)))
927 safe_path = _clean_up_path_for_archive(p)
929 if stat.S_ISDIR(st.st_mode):
931 m = from_path(p, statinfo=st, archive_path=safe_path,
932 save_symlinks=save_symlinks)
934 print(m.path, file=sys.stderr)
935 m.write(output_file, include_path=write_paths)
937 start_dir = os.getcwd()
939 for (p, st) in recursive_dirlist(paths, xdev=xdev):
940 dirlist_dir = os.getcwd()
942 safe_path = _clean_up_path_for_archive(p)
943 m = from_path(p, statinfo=st, archive_path=safe_path,
944 save_symlinks=save_symlinks)
946 print(m.path, file=sys.stderr)
947 m.write(output_file, include_path=write_paths)
948 os.chdir(dirlist_dir)
953 def _set_up_path(meta, create_symlinks=True):
954 # Allow directories to exist as a special case -- might have
955 # been created by an earlier longer path.
959 parent = os.path.dirname(meta.path)
962 meta.create_path(meta.path, create_symlinks=create_symlinks)
965 all_fields = frozenset(['path',
982 def summary_bytes(meta, numeric_ids = False, classification = None,
983 human_readable = False):
984 """Return bytes containing the "ls -l" style listing for meta.
985 Classification may be "all", "type", or None."""
986 user_str = group_str = size_or_dev_str = b'?'
987 symlink_target = None
990 mode_str = xstat.mode_str(meta.mode).encode('ascii')
991 symlink_target = meta.symlink_target
992 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
993 mtime_str = strftime('%Y-%m-%d %H:%M',
994 time.localtime(mtime_secs)).encode('ascii')
995 if meta.user and not numeric_ids:
997 elif meta.uid != None:
998 user_str = str(meta.uid).encode()
999 if meta.group and not numeric_ids:
1000 group_str = meta.group
1001 elif meta.gid != None:
1002 group_str = str(meta.gid).encode()
1003 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
1005 size_or_dev_str = ('%d,%d' % (os.major(meta.rdev),
1006 os.minor(meta.rdev))).encode()
1007 elif meta.size != None:
1009 size_or_dev_str = format_filesize(meta.size).encode()
1011 size_or_dev_str = str(meta.size).encode()
1013 size_or_dev_str = b'-'
1015 classification_str = \
1016 xstat.classification_str(meta.mode,
1017 classification == 'all').encode()
1019 mode_str = b'?' * 10
1020 mtime_str = b'????-??-?? ??:??'
1021 classification_str = b'?'
1025 name += classification_str
1027 name += b' -> ' + meta.symlink_target
1029 return b'%-10s %-11s %11s %16s %s' % (mode_str,
1030 user_str + b'/' + group_str,
1036 def detailed_bytes(meta, fields = None):
1037 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
1038 # 0", "link-target:", etc.
1043 if 'path' in fields:
1044 path = meta.path or b''
1045 result.append(b'path: ' + path)
1046 if 'mode' in fields:
1047 result.append(b'mode: %o (%s)'
1048 % (meta.mode, xstat.mode_str(meta.mode).encode('ascii')))
1049 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1050 result.append(b'link-target: ' + meta.symlink_target)
1051 if 'rdev' in fields:
1053 result.append(b'rdev: %d,%d' % (os.major(meta.rdev),
1054 os.minor(meta.rdev)))
1056 result.append(b'rdev: 0')
1057 if 'size' in fields and meta.size is not None:
1058 result.append(b'size: %d' % meta.size)
1060 result.append(b'uid: %d' % meta.uid)
1062 result.append(b'gid: %d' % meta.gid)
1063 if 'user' in fields:
1064 result.append(b'user: ' + meta.user)
1065 if 'group' in fields:
1066 result.append(b'group: ' + meta.group)
1067 if 'atime' in fields:
1068 # If we don't have xstat.lutime, that means we have to use
1069 # utime(), and utime() has no way to set the mtime/atime of a
1070 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1071 # so let's not report it. (That way scripts comparing
1072 # before/after won't trigger.)
1073 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1074 result.append(b'atime: ' + xstat.fstime_to_sec_bytes(meta.atime))
1076 result.append(b'atime: 0')
1077 if 'mtime' in fields:
1078 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1079 result.append(b'mtime: ' + xstat.fstime_to_sec_bytes(meta.mtime))
1081 result.append(b'mtime: 0')
1082 if 'ctime' in fields:
1083 result.append(b'ctime: ' + xstat.fstime_to_sec_bytes(meta.ctime))
1084 if 'linux-attr' in fields and meta.linux_attr:
1085 result.append(b'linux-attr: %x' % meta.linux_attr)
1086 if 'linux-xattr' in fields and meta.linux_xattr:
1087 for name, value in meta.linux_xattr:
1088 result.append(b'linux-xattr: %s -> %s' % (name, value))
1089 if 'posix1e-acl' in fields and meta.posix1e_acl:
1090 acl = meta.posix1e_acl[0]
1091 result.append(b'posix1e-acl: ' + acl + b'\n')
1092 if stat.S_ISDIR(meta.mode):
1093 def_acl = meta.posix1e_acl[2]
1094 result.append(b'posix1e-acl-default: ' + def_acl + b'\n')
1095 return b'\n'.join(result)
1098 class _ArchiveIterator:
1101 return Metadata.read(self._file)
1103 raise StopIteration()
1110 def __init__(self, file):
1114 def display_archive(file, out):
1117 for meta in _ArchiveIterator(file):
1120 out.write(detailed_bytes(meta))
1124 for meta in _ArchiveIterator(file):
1125 out.write(summary_bytes(meta))
1128 for meta in _ArchiveIterator(file):
1130 log('bup: no metadata path, but asked to only display path'
1131 ' (increase verbosity?)')
1133 out.write(meta.path)
1137 def start_extract(file, create_symlinks=True):
1138 for meta in _ArchiveIterator(file):
1139 if not meta: # Hit end record.
1142 print(path_msg(meta.path), file=sys.stderr)
1143 xpath = _clean_up_extract_path(meta.path)
1145 add_error(Exception('skipping risky path "%s"'
1146 % path_msg(meta.path)))
1149 _set_up_path(meta, create_symlinks=create_symlinks)
1152 def finish_extract(file, restore_numeric_ids=False):
1154 for meta in _ArchiveIterator(file):
1155 if not meta: # Hit end record.
1157 xpath = _clean_up_extract_path(meta.path)
1159 add_error(Exception('skipping risky path "%s"'
1160 % path_msg(meta.path)))
1162 if os.path.isdir(meta.path):
1163 all_dirs.append(meta)
1166 print(path_msg(meta.path), file=sys.stderr)
1167 meta.apply_to_path(path=xpath,
1168 restore_numeric_ids=restore_numeric_ids)
1169 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1170 for dir in all_dirs:
1171 # Don't need to check xpath -- won't be in all_dirs if not OK.
1172 xpath = _clean_up_extract_path(dir.path)
1174 print(path_msg(dir.path), file=sys.stderr)
1175 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1178 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1179 # For now, just store all the directories and handle them last,
1182 for meta in _ArchiveIterator(file):
1183 if not meta: # Hit end record.
1185 xpath = _clean_up_extract_path(meta.path)
1187 add_error(Exception('skipping risky path "%s"'
1188 % path_msg(meta.path)))
1192 print('+', path_msg(meta.path), file=sys.stderr)
1193 _set_up_path(meta, create_symlinks=create_symlinks)
1194 if os.path.isdir(meta.path):
1195 all_dirs.append(meta)
1198 print('=', path_msg(meta.path), file=sys.stderr)
1199 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1200 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1201 for dir in all_dirs:
1202 # Don't need to check xpath -- won't be in all_dirs if not OK.
1203 xpath = _clean_up_extract_path(dir.path)
1205 print('=', path_msg(xpath), file=sys.stderr)
1206 # Shouldn't have to check for risky paths here (omitted above).
1207 dir.apply_to_path(path=dir.path,
1208 restore_numeric_ids=restore_numeric_ids)