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 if not (sys.platform.startswith('cygwin') or
47 sys.platform.startswith('darwin') or
48 sys.platform.startswith('netbsd')):
49 log('Warning: POSIX ACL support missing; recompile with libacl1-dev/libacl-devel.\n')
50 read_acl = apply_acl = None
53 from bup._helpers import get_linux_file_attr, set_linux_file_attr
55 # No need for a warning here; the only reason they won't exist is that we're
56 # not on Linux, in which case files don't have any linux attrs anyway, so
57 # lacking the functions isn't a problem.
58 get_linux_file_attr = set_linux_file_attr = None
61 # See the bup_get_linux_file_attr() comments.
62 _suppress_linux_file_attr = \
63 sys.byteorder == 'big' and struct.calcsize('@l') > struct.calcsize('@i')
65 def check_linux_file_attr_api():
66 global get_linux_file_attr, set_linux_file_attr
67 if not (get_linux_file_attr or set_linux_file_attr):
69 if _suppress_linux_file_attr:
70 log('Warning: Linux attr support disabled (see "bup help index").\n')
71 get_linux_file_attr = set_linux_file_attr = None
74 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
76 # Q: Consider hardlink support?
77 # Q: Is it OK to store raw linux attr (chattr) flags?
78 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
79 # Q: Is the application of posix1e has_extended() correct?
80 # Q: Is one global --numeric-ids argument sufficient?
81 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
82 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
84 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
85 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
86 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
87 # FIXME: Consider pack('vvvvsss', ...) optimization.
91 # osx (varies between hfs and hfs+):
92 # type - regular dir char block fifo socket ...
93 # perms - rwxrwxrwxsgt
94 # times - ctime atime mtime
97 # hard-link-info (hfs+ only)
100 # attributes-osx see chflags
106 # type - regular dir ...
107 # times - creation, modification, posix change, access
110 # attributes - see attrib
112 # forks (alternate data streams)
116 # type - regular dir ...
117 # perms - rwxrwxrwx (maybe - see wikipedia)
118 # times - creation, modification, access
119 # attributes - see attrib
123 _have_lchmod = hasattr(os, 'lchmod')
126 def _clean_up_path_for_archive(p):
127 # Not the most efficient approach.
130 # Take everything after any '/../'.
131 pos = result.rfind(b'/../')
133 result = result[result.rfind(b'/../') + 4:]
135 # Take everything after any remaining '../'.
136 if result.startswith(b"../"):
139 # Remove any '/./' sequences.
140 pos = result.find(b'/./')
142 result = result[0:pos] + b'/' + result[pos + 3:]
143 pos = result.find(b'/./')
145 # Remove any leading '/'s.
146 result = result.lstrip(b'/')
148 # Replace '//' with '/' everywhere.
149 pos = result.find(b'//')
151 result = result[0:pos] + b'/' + result[pos + 2:]
152 pos = result.find(b'//')
154 # Take everything after any remaining './'.
155 if result.startswith(b'./'):
158 # Take everything before any remaining '/.'.
159 if result.endswith(b'/.'):
162 if result == b'' or result.endswith(b'/..'):
169 if p.startswith(b'/'):
171 if p.find(b'/../') != -1:
173 if p.startswith(b'../'):
175 if p.endswith(b'/..'):
180 def _clean_up_extract_path(p):
181 result = p.lstrip(b'/')
184 elif _risky_path(result):
190 # These tags are currently conceptually private to Metadata, and they
191 # must be unique, and must *never* be changed.
194 _rec_tag_common_v1 = 2 # times, user, group, type, perms, etc. (legacy/broken)
195 _rec_tag_symlink_target = 3
196 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
197 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
198 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
199 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
200 _rec_tag_hardlink_target = 8 # hard link target path
201 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
202 _rec_tag_common_v3 = 10 # adds optional size to v2
204 _warned_about_attr_einval = None
207 class ApplyError(Exception):
208 # Thrown when unable to apply any given bit of metadata to a path.
213 # Metadata is stored as a sequence of tagged binary records. Each
214 # record will have some subset of add, encode, load, create, and
215 # apply methods, i.e. _add_foo...
217 # We do allow an "empty" object as a special case, i.e. no
218 # records. One can be created by trying to write Metadata(), and
219 # for such an object, read() will return None. This is used by
220 # "bup save", for example, as a placeholder in cases where
223 # NOTE: if any relevant fields are added or removed, be sure to
224 # update same_file() below.
228 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
229 # must be non-negative and < 10**9.
231 def _add_common(self, path, st):
232 assert(st.st_uid >= 0)
233 assert(st.st_gid >= 0)
234 self.size = st.st_size
237 self.atime = st.st_atime
238 self.mtime = st.st_mtime
239 self.ctime = st.st_ctime
240 self.user = self.group = b''
241 entry = pwd_from_uid(st.st_uid)
243 self.user = entry.pw_name
244 entry = grp_from_gid(st.st_gid)
246 self.group = entry.gr_name
247 self.mode = st.st_mode
248 # Only collect st_rdev if we might need it for a mknod()
249 # during restore. On some platforms (i.e. kFreeBSD), it isn't
250 # stable for other file types. For example "cp -a" will
251 # change it for a plain file.
252 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
253 self.rdev = st.st_rdev
257 def _same_common(self, other):
258 """Return true or false to indicate similarity in the hardlink sense."""
259 return self.uid == other.uid \
260 and self.gid == other.gid \
261 and self.rdev == other.rdev \
262 and self.mtime == other.mtime \
263 and self.ctime == other.ctime \
264 and self.user == other.user \
265 and self.group == other.group \
266 and self.size == other.size
268 def _encode_common(self):
271 atime = xstat.nsecs_to_timespec(self.atime)
272 mtime = xstat.nsecs_to_timespec(self.mtime)
273 ctime = xstat.nsecs_to_timespec(self.ctime)
274 result = vint.pack('vvsvsvvVvVvVv',
287 self.size if self.size is not None else -1)
290 def _load_common_rec(self, port, version=3):
292 # Added trailing size to v2, negative when None.
293 unpack_fmt = 'vvsvsvvVvVvVv'
295 unpack_fmt = 'vvsvsvvVvVvV'
297 unpack_fmt = 'VVsVsVvVvVvV'
299 raise Exception('unexpected common_rec version %d' % version)
300 data = vint.read_bvec(port)
301 values = vint.unpack(unpack_fmt, data)
303 (self.mode, self.uid, self.user, self.gid, self.group,
305 self.atime, atime_ns,
306 self.mtime, mtime_ns,
307 self.ctime, ctime_ns, size) = values
311 (self.mode, self.uid, self.user, self.gid, self.group,
313 self.atime, atime_ns,
314 self.mtime, mtime_ns,
315 self.ctime, ctime_ns) = values
316 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
317 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
318 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
320 def _recognized_file_type(self):
321 return stat.S_ISREG(self.mode) \
322 or stat.S_ISDIR(self.mode) \
323 or stat.S_ISCHR(self.mode) \
324 or stat.S_ISBLK(self.mode) \
325 or stat.S_ISFIFO(self.mode) \
326 or stat.S_ISSOCK(self.mode) \
327 or stat.S_ISLNK(self.mode)
329 def _create_via_common_rec(self, path, create_symlinks=True):
331 raise ApplyError('no metadata - cannot create path '
334 # If the path already exists and is a dir, try rmdir.
335 # If the path already exists and is anything else, try unlink.
338 st = xstat.lstat(path)
340 if e.errno != errno.ENOENT:
343 if stat.S_ISDIR(st.st_mode):
347 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
348 raise Exception('refusing to overwrite non-empty dir '
354 if stat.S_ISREG(self.mode):
355 assert(self._recognized_file_type())
356 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
358 elif stat.S_ISDIR(self.mode):
359 assert(self._recognized_file_type())
360 os.mkdir(path, 0o700)
361 elif stat.S_ISCHR(self.mode):
362 assert(self._recognized_file_type())
363 os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
364 elif stat.S_ISBLK(self.mode):
365 assert(self._recognized_file_type())
366 os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
367 elif stat.S_ISFIFO(self.mode):
368 assert(self._recognized_file_type())
369 os.mkfifo(path, 0o600 | stat.S_IFIFO)
370 elif stat.S_ISSOCK(self.mode):
372 os.mknod(path, 0o600 | stat.S_IFSOCK)
374 if e.errno in (errno.EINVAL, errno.EPERM):
375 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
379 elif stat.S_ISLNK(self.mode):
380 assert(self._recognized_file_type())
381 if self.symlink_target and create_symlinks:
382 # on MacOS, symlink() permissions depend on umask, and there's
383 # no way to chown a symlink after creating it, so we have to
385 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
387 os.symlink(self.symlink_target, path)
390 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
392 assert(not self._recognized_file_type())
393 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
394 % (path_msg(path), self.mode))
396 def _apply_common_rec(self, path, restore_numeric_ids=False):
398 raise ApplyError('no metadata - cannot apply to ' + path_msg(path))
400 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
401 # EACCES errors at this stage are fatal for the current path.
402 if lutime and stat.S_ISLNK(self.mode):
404 lutime(path, (self.atime, self.mtime))
406 if e.errno == errno.EACCES:
407 raise ApplyError('lutime: %s' % e)
412 utime(path, (self.atime, self.mtime))
414 if e.errno == errno.EACCES:
415 raise ApplyError('utime: %s' % e)
419 uid = gid = -1 # By default, do nothing.
421 if self.uid is not None:
423 if self.gid is not None:
425 if not restore_numeric_ids:
426 if self.uid != 0 and self.user:
427 entry = pwd_from_name(self.user)
430 if self.gid != 0 and self.group:
431 entry = grp_from_name(self.group)
434 else: # not superuser - only consider changing the group/gid
435 user_gids = os.getgroups()
436 if self.gid in user_gids:
438 if not restore_numeric_ids and self.gid != 0:
439 # The grp might not exist on the local system.
440 grps = filter(None, [grp_from_gid(x) for x in user_gids])
441 if self.group in [x.gr_name for x in grps]:
442 g = grp_from_name(self.group)
446 if uid != -1 or gid != -1:
448 os.lchown(path, uid, gid)
450 if e.errno == errno.EPERM:
451 add_error('lchown: %s' % e)
452 elif sys.platform.startswith('cygwin') \
453 and e.errno == errno.EINVAL:
454 add_error('lchown: unknown uid/gid (%d/%d) for %s'
455 % (uid, gid, path_msg(path)))
461 os.lchmod(path, stat.S_IMODE(self.mode))
462 except errno.ENOSYS: # Function not implemented
464 elif not stat.S_ISLNK(self.mode):
465 os.chmod(path, stat.S_IMODE(self.mode))
470 def _encode_path(self):
472 return vint.pack('s', self.path)
476 def _load_path_rec(self, port):
477 self.path = vint.unpack('s', vint.read_bvec(port))[0]
482 def _add_symlink_target(self, path, st):
484 if stat.S_ISLNK(st.st_mode):
485 self.symlink_target = os.readlink(path)
487 add_error('readlink: %s' % e)
489 def _encode_symlink_target(self):
490 return self.symlink_target
492 def _load_symlink_target_rec(self, port):
493 target = vint.read_bvec(port)
494 self.symlink_target = target
495 if self.size is None:
496 self.size = len(target)
498 assert(self.size == len(target))
503 def _add_hardlink_target(self, target):
504 self.hardlink_target = target
506 def _same_hardlink_target(self, other):
507 """Return true or false to indicate similarity in the hardlink sense."""
508 return self.hardlink_target == other.hardlink_target
510 def _encode_hardlink_target(self):
511 return self.hardlink_target
513 def _load_hardlink_target_rec(self, port):
514 self.hardlink_target = vint.read_bvec(port)
517 ## POSIX1e ACL records
519 # Recorded as a list:
520 # [txt_id_acl, num_id_acl]
521 # or, if a directory:
522 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
523 # The numeric/text distinction only matters when reading/restoring
525 def _add_posix1e_acl(self, path, st):
528 if not stat.S_ISLNK(st.st_mode):
529 isdir = 1 if stat.S_ISDIR(st.st_mode) else 0
530 self.posix1e_acl = read_acl(path, isdir)
532 def _same_posix1e_acl(self, other):
533 """Return true or false to indicate similarity in the hardlink sense."""
534 return self.posix1e_acl == other.posix1e_acl
536 def _encode_posix1e_acl(self):
537 # Encode as two strings (w/default ACL string possibly empty).
539 acls = self.posix1e_acl
541 return vint.pack('ssss', acls[0], acls[1], b'', b'')
542 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
546 def _load_posix1e_acl_rec(self, port):
547 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
548 if acl_rep[2] == b'':
549 acl_rep = acl_rep[:2]
550 self.posix1e_acl = acl_rep
552 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
553 if not self.posix1e_acl:
557 add_error("%s: can't restore ACLs; posix1e support missing.\n"
562 acls = self.posix1e_acl
563 offs = 1 if restore_numeric_ids else 0
565 apply_acl(path, acls[offs], acls[offs + 2])
567 apply_acl(path, acls[offs])
569 if e.errno == errno.EINVAL:
570 # libacl returns with errno set to EINVAL if a user
571 # (or group) doesn't exist
572 raise ApplyError("POSIX1e ACL: can't create %r for %r"
573 % (acls, path_msg(path)))
574 elif e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
575 raise ApplyError('POSIX1e ACL applyto: %s' % e)
580 ## Linux attributes (lsattr(1), chattr(1))
582 def _add_linux_attr(self, path, st):
583 check_linux_file_attr_api()
584 if not get_linux_file_attr: return
585 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
587 attr = get_linux_file_attr(path)
589 self.linux_attr = attr
591 if e.errno == errno.EACCES:
592 add_error('read Linux attr: %s' % e)
593 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
594 # Assume filesystem doesn't support attrs.
596 elif e.errno == EINVAL:
597 global _warned_about_attr_einval
598 if not _warned_about_attr_einval:
599 log("Ignoring attr EINVAL;"
600 + " if you're not using ntfs-3g, please report: "
601 + path_msg(path) + '\n')
602 _warned_about_attr_einval = True
607 def _same_linux_attr(self, other):
608 """Return true or false to indicate similarity in the hardlink sense."""
609 return self.linux_attr == other.linux_attr
611 def _encode_linux_attr(self):
613 return vint.pack('V', self.linux_attr)
617 def _load_linux_attr_rec(self, port):
618 data = vint.read_bvec(port)
619 self.linux_attr = vint.unpack('V', data)[0]
621 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
623 check_linux_file_attr_api()
624 if not set_linux_file_attr:
625 add_error("%s: can't restore linuxattrs: "
626 "linuxattr support missing.\n" % path_msg(path))
629 set_linux_file_attr(path, self.linux_attr)
631 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
632 raise ApplyError('Linux chattr: %s (0x%s)'
633 % (e, hex(self.linux_attr)))
634 elif e.errno == EINVAL:
635 msg = "if you're not using ntfs-3g, please report"
636 raise ApplyError('Linux chattr: %s (0x%s) (%s)'
637 % (e, hex(self.linux_attr), msg))
642 ## Linux extended attributes (getfattr(1), setfattr(1))
644 def _add_linux_xattr(self, path, st):
647 self.linux_xattr = xattr.get_all(path, nofollow=True)
648 except EnvironmentError as e:
649 if e.errno != errno.EOPNOTSUPP:
652 def _same_linux_xattr(self, other):
653 """Return true or false to indicate similarity in the hardlink sense."""
654 return self.linux_xattr == other.linux_xattr
656 def _encode_linux_xattr(self):
658 result = vint.pack('V', len(self.linux_xattr))
659 for name, value in self.linux_xattr:
660 result += vint.pack('ss', name, value)
665 def _load_linux_xattr_rec(self, file):
666 data = vint.read_bvec(file)
667 memfile = BytesIO(data)
669 for i in range(vint.read_vuint(memfile)):
670 key = vint.read_bvec(memfile)
671 value = vint.read_bvec(memfile)
672 result.append((key, value))
673 self.linux_xattr = result
675 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
678 add_error("%s: can't restore xattr; xattr support missing.\n"
681 if not self.linux_xattr:
684 existing_xattrs = set(xattr.list(path, nofollow=True))
686 if e.errno == errno.EACCES:
687 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
690 for k, v in self.linux_xattr:
691 if k not in existing_xattrs \
692 or v != xattr.get(path, k, nofollow=True):
694 xattr.set(path, k, v, nofollow=True)
696 if e.errno == errno.EPERM \
697 or e.errno == errno.EOPNOTSUPP:
698 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
701 existing_xattrs -= frozenset([k])
702 for k in existing_xattrs:
704 xattr.remove(path, k, nofollow=True)
706 if e.errno in (errno.EPERM, errno.EACCES):
707 raise ApplyError('xattr.remove %r: %s' % (path_msg(path), e))
712 self.mode = self.uid = self.gid = self.user = self.group = None
713 self.atime = self.mtime = self.ctime = None
717 self.symlink_target = None
718 self.hardlink_target = None
719 self.linux_attr = None
720 self.linux_xattr = None
721 self.posix1e_acl = None
723 def __eq__(self, other):
724 if not isinstance(other, Metadata): return False
725 if self.mode != other.mode: return False
726 if self.mtime != other.mtime: return False
727 if self.ctime != other.ctime: return False
728 if self.atime != other.atime: return False
729 if self.path != other.path: return False
730 if self.uid != other.uid: return False
731 if self.gid != other.gid: return False
732 if self.size != other.size: return False
733 if self.user != other.user: return False
734 if self.group != other.group: return False
735 if self.symlink_target != other.symlink_target: return False
736 if self.hardlink_target != other.hardlink_target: return False
737 if self.linux_attr != other.linux_attr: return False
738 if self.posix1e_acl != other.posix1e_acl: return False
741 def __ne__(self, other):
742 return not self.__eq__(other)
745 return hash((self.mode,
756 self.hardlink_target,
761 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
762 if self.path is not None:
763 result += ' path:' + repr(self.path)
764 if self.mode is not None:
765 result += ' mode: %o (%s)' % (self.mode, xstat.mode_str(self.mode))
766 if self.uid is not None:
767 result += ' uid:' + str(self.uid)
768 if self.gid is not None:
769 result += ' gid:' + str(self.gid)
770 if self.user is not None:
771 result += ' user:' + repr(self.user)
772 if self.group is not None:
773 result += ' group:' + repr(self.group)
774 if self.size is not None:
775 result += ' size:' + repr(self.size)
776 for name, val in (('atime', self.atime),
777 ('mtime', self.mtime),
778 ('ctime', self.ctime)):
780 result += ' %s:%r (%d)' \
782 strftime('%Y-%m-%d %H:%M %z',
783 gmtime(xstat.fstime_floor_secs(val))),
786 return ''.join(result)
788 def write(self, port, 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 vint.write_vuint(port, tag)
801 vint.write_bvec(port, data)
802 vint.write_vuint(port, _rec_tag_end)
804 def encode(self, include_path=True):
806 self.write(port, include_path)
807 return port.getvalue()
810 return deepcopy(self)
814 # This method should either return a valid Metadata object,
815 # return None if there was no information at all (just a
816 # _rec_tag_end), throw EOFError if there was nothing at all to
817 # read, or throw an Exception if a valid object could not be
819 tag = vint.read_vuint(port)
820 if tag == _rec_tag_end:
822 try: # From here on, EOF is an error.
824 while True: # only exit is error (exception) or _rec_tag_end
825 if tag == _rec_tag_path:
826 result._load_path_rec(port)
827 elif tag == _rec_tag_common_v3:
828 result._load_common_rec(port, version=3)
829 elif tag == _rec_tag_common_v2:
830 result._load_common_rec(port, version=2)
831 elif tag == _rec_tag_symlink_target:
832 result._load_symlink_target_rec(port)
833 elif tag == _rec_tag_hardlink_target:
834 result._load_hardlink_target_rec(port)
835 elif tag == _rec_tag_posix1e_acl:
836 result._load_posix1e_acl_rec(port)
837 elif tag == _rec_tag_linux_attr:
838 result._load_linux_attr_rec(port)
839 elif tag == _rec_tag_linux_xattr:
840 result._load_linux_xattr_rec(port)
841 elif tag == _rec_tag_end:
843 elif tag == _rec_tag_common_v1: # Should be very rare.
844 result._load_common_rec(port, version=1)
845 else: # unknown record
847 tag = vint.read_vuint(port)
849 raise Exception("EOF while reading Metadata")
852 return stat.S_ISDIR(self.mode)
854 def create_path(self, path, create_symlinks=True):
855 self._create_via_common_rec(path, create_symlinks=create_symlinks)
857 def apply_to_path(self, path=None, restore_numeric_ids=False):
858 # apply metadata to path -- file must exist
862 raise Exception('Metadata.apply_to_path() called with no path')
863 if not self._recognized_file_type():
864 add_error('not applying metadata to "%s"' % path_msg(path)
865 + ' with unrecognized mode "0x%x"\n' % self.mode)
867 num_ids = restore_numeric_ids
868 for apply_metadata in (self._apply_common_rec,
869 self._apply_posix1e_acl_rec,
870 self._apply_linux_attr_rec,
871 self._apply_linux_xattr_rec):
873 apply_metadata(path, restore_numeric_ids=num_ids)
874 except ApplyError as e:
877 def same_file(self, other):
878 """Compare this to other for equivalency. Return true if
879 their information implies they could represent the same file
880 on disk, in the hardlink sense. Assume they're both regular
882 return self._same_common(other) \
883 and self._same_hardlink_target(other) \
884 and self._same_posix1e_acl(other) \
885 and self._same_linux_attr(other) \
886 and self._same_linux_xattr(other)
889 def from_path(path, statinfo=None, archive_path=None,
890 save_symlinks=True, hardlink_target=None,
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)