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 copy import deepcopy
10 from errno import EACCES, EINVAL, ENOTTY, ENOSYS, EOPNOTSUPP
11 from io import BytesIO
12 from time import gmtime, strftime
13 import errno, os, sys, stat, time, socket, struct
15 from bup import vint, xstat
16 from bup.drecurse import recursive_dirlist
17 from bup.helpers import add_error, mkdirp, log, is_superuser, format_filesize
18 from bup.io import path_msg
19 from bup.pwdgrp import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
20 from bup.xstat import utime, lutime
23 if sys.platform.startswith('linux'):
24 # prefer python-pyxattr (it's a lot faster), but fall back to python-xattr
25 # as the two are incompatible and only one can be installed on a system
29 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
30 if xattr and getattr(xattr, 'get_all', None) is None:
32 from xattr import pyxattr_compat as xattr
33 if not isinstance(xattr.NS_USER, bytes):
38 log('Warning: python-xattr module is too old; '
39 'upgrade or install python-pyxattr instead.\n')
42 from bup._helpers import read_acl, apply_acl
44 read_acl = apply_acl = None
47 from bup._helpers import get_linux_file_attr, set_linux_file_attr
49 # No need for a warning here; the only reason they won't exist is that we're
50 # not on Linux, in which case files don't have any linux attrs anyway, so
51 # lacking the functions isn't a problem.
52 get_linux_file_attr = set_linux_file_attr = None
55 # See the bup_get_linux_file_attr() comments.
56 _suppress_linux_file_attr = \
57 sys.byteorder == 'big' and struct.calcsize('@l') > struct.calcsize('@i')
59 def check_linux_file_attr_api():
60 global get_linux_file_attr, set_linux_file_attr
61 if not (get_linux_file_attr or set_linux_file_attr):
63 if _suppress_linux_file_attr:
64 log('Warning: Linux attr support disabled (see "bup help index").\n')
65 get_linux_file_attr = set_linux_file_attr = None
68 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
70 # Q: Consider hardlink support?
71 # Q: Is it OK to store raw linux attr (chattr) flags?
72 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
73 # Q: Is the application of posix1e has_extended() correct?
74 # Q: Is one global --numeric-ids argument sufficient?
75 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
76 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
78 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
79 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
80 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
81 # FIXME: Consider pack('vvvvsss', ...) optimization.
85 # osx (varies between hfs and hfs+):
86 # type - regular dir char block fifo socket ...
87 # perms - rwxrwxrwxsgt
88 # times - ctime atime mtime
91 # hard-link-info (hfs+ only)
94 # attributes-osx see chflags
100 # type - regular dir ...
101 # times - creation, modification, posix change, access
104 # attributes - see attrib
106 # forks (alternate data streams)
110 # type - regular dir ...
111 # perms - rwxrwxrwx (maybe - see wikipedia)
112 # times - creation, modification, access
113 # attributes - see attrib
117 _have_lchmod = hasattr(os, 'lchmod')
120 def _clean_up_path_for_archive(p):
121 # Not the most efficient approach.
124 # Take everything after any '/../'.
125 pos = result.rfind(b'/../')
127 result = result[result.rfind(b'/../') + 4:]
129 # Take everything after any remaining '../'.
130 if result.startswith(b"../"):
133 # Remove any '/./' sequences.
134 pos = result.find(b'/./')
136 result = result[0:pos] + b'/' + result[pos + 3:]
137 pos = result.find(b'/./')
139 # Remove any leading '/'s.
140 result = result.lstrip(b'/')
142 # Replace '//' with '/' everywhere.
143 pos = result.find(b'//')
145 result = result[0:pos] + b'/' + result[pos + 2:]
146 pos = result.find(b'//')
148 # Take everything after any remaining './'.
149 if result.startswith(b'./'):
152 # Take everything before any remaining '/.'.
153 if result.endswith(b'/.'):
156 if result == b'' or result.endswith(b'/..'):
163 if p.startswith(b'/'):
165 if p.find(b'/../') != -1:
167 if p.startswith(b'../'):
169 if p.endswith(b'/..'):
174 def _clean_up_extract_path(p):
175 result = p.lstrip(b'/')
178 elif _risky_path(result):
184 # These tags are currently conceptually private to Metadata, and they
185 # must be unique, and must *never* be changed.
188 _rec_tag_common_v1 = 2 # times, user, group, type, perms, etc. (legacy/broken)
189 _rec_tag_symlink_target = 3
190 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
191 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
192 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
193 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
194 _rec_tag_hardlink_target = 8 # hard link target path
195 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
196 _rec_tag_common_v3 = 10 # adds optional size to v2
198 _warned_about_attr_einval = None
201 class ApplyError(Exception):
202 # Thrown when unable to apply any given bit of metadata to a path.
207 # Metadata is stored as a sequence of tagged binary records. Each
208 # record will have some subset of add, encode, load, create, and
209 # apply methods, i.e. _add_foo...
211 # We do allow an "empty" object as a special case, i.e. no
212 # records. One can be created by trying to write Metadata(), and
213 # for such an object, read() will return None. This is used by
214 # "bup save", for example, as a placeholder in cases where
217 # NOTE: if any relevant fields are added or removed, be sure to
218 # update same_file() below.
222 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
223 # must be non-negative and < 10**9.
225 def _add_common(self, path, st):
226 assert(st.st_uid >= 0)
227 assert(st.st_gid >= 0)
228 self.size = st.st_size
231 self.atime = st.st_atime
232 self.mtime = st.st_mtime
233 self.ctime = st.st_ctime
234 self.user = self.group = b''
235 entry = pwd_from_uid(st.st_uid)
237 self.user = entry.pw_name
238 entry = grp_from_gid(st.st_gid)
240 self.group = entry.gr_name
241 self.mode = st.st_mode
242 # Only collect st_rdev if we might need it for a mknod()
243 # during restore. On some platforms (i.e. kFreeBSD), it isn't
244 # stable for other file types. For example "cp -a" will
245 # change it for a plain file.
246 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
247 self.rdev = st.st_rdev
251 def _same_common(self, other):
252 """Return true or false to indicate similarity in the hardlink sense."""
253 return self.uid == other.uid \
254 and self.gid == other.gid \
255 and self.rdev == other.rdev \
256 and self.mtime == other.mtime \
257 and self.ctime == other.ctime \
258 and self.user == other.user \
259 and self.group == other.group \
260 and self.size == other.size
262 def _encode_common(self):
265 atime = xstat.nsecs_to_timespec(self.atime)
266 mtime = xstat.nsecs_to_timespec(self.mtime)
267 ctime = xstat.nsecs_to_timespec(self.ctime)
268 result = vint.pack('vvsvsvvVvVvVv',
281 self.size if self.size is not None else -1)
284 def _load_common_rec(self, port, version=3):
286 # Added trailing size to v2, negative when None.
287 unpack_fmt = 'vvsvsvvVvVvVv'
289 unpack_fmt = 'vvsvsvvVvVvV'
291 unpack_fmt = 'VVsVsVvVvVvV'
293 raise Exception('unexpected common_rec version %d' % version)
294 data = vint.read_bvec(port)
295 values = vint.unpack(unpack_fmt, data)
297 (self.mode, self.uid, self.user, self.gid, self.group,
299 self.atime, atime_ns,
300 self.mtime, mtime_ns,
301 self.ctime, ctime_ns, size) = values
305 (self.mode, self.uid, self.user, self.gid, self.group,
307 self.atime, atime_ns,
308 self.mtime, mtime_ns,
309 self.ctime, ctime_ns) = values
310 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
311 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
312 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
314 def _recognized_file_type(self):
315 return stat.S_ISREG(self.mode) \
316 or stat.S_ISDIR(self.mode) \
317 or stat.S_ISCHR(self.mode) \
318 or stat.S_ISBLK(self.mode) \
319 or stat.S_ISFIFO(self.mode) \
320 or stat.S_ISSOCK(self.mode) \
321 or stat.S_ISLNK(self.mode)
323 def _create_via_common_rec(self, path, create_symlinks=True):
325 raise ApplyError('no metadata - cannot create path '
328 # If the path already exists and is a dir, try rmdir.
329 # If the path already exists and is anything else, try unlink.
332 st = xstat.lstat(path)
334 if e.errno != errno.ENOENT:
337 if stat.S_ISDIR(st.st_mode):
341 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
342 raise Exception('refusing to overwrite non-empty dir '
348 if stat.S_ISREG(self.mode):
349 assert(self._recognized_file_type())
350 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
352 elif stat.S_ISDIR(self.mode):
353 assert(self._recognized_file_type())
354 os.mkdir(path, 0o700)
355 elif stat.S_ISCHR(self.mode):
356 assert(self._recognized_file_type())
357 os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
358 elif stat.S_ISBLK(self.mode):
359 assert(self._recognized_file_type())
360 os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
361 elif stat.S_ISFIFO(self.mode):
362 assert(self._recognized_file_type())
363 os.mkfifo(path, 0o600 | stat.S_IFIFO)
364 elif stat.S_ISSOCK(self.mode):
366 os.mknod(path, 0o600 | stat.S_IFSOCK)
368 if e.errno in (errno.EINVAL, errno.EPERM):
369 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
373 elif stat.S_ISLNK(self.mode):
374 assert(self._recognized_file_type())
375 if self.symlink_target and create_symlinks:
376 # on MacOS, symlink() permissions depend on umask, and there's
377 # no way to chown a symlink after creating it, so we have to
379 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
381 os.symlink(self.symlink_target, path)
384 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
386 assert(not self._recognized_file_type())
387 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
388 % (path_msg(path), self.mode))
390 def _apply_common_rec(self, path, restore_numeric_ids=False):
392 raise ApplyError('no metadata - cannot apply to ' + path_msg(path))
394 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
395 # EACCES errors at this stage are fatal for the current path.
396 if lutime and stat.S_ISLNK(self.mode):
398 lutime(path, (self.atime, self.mtime))
400 if e.errno == errno.EACCES:
401 raise ApplyError('lutime: %s' % e)
406 utime(path, (self.atime, self.mtime))
408 if e.errno == errno.EACCES:
409 raise ApplyError('utime: %s' % e)
413 uid = gid = -1 # By default, do nothing.
415 if self.uid is not None:
417 if self.gid is not None:
419 if not restore_numeric_ids:
420 if self.uid != 0 and self.user:
421 entry = pwd_from_name(self.user)
424 if self.gid != 0 and self.group:
425 entry = grp_from_name(self.group)
428 else: # not superuser - only consider changing the group/gid
429 user_gids = os.getgroups()
430 if self.gid in user_gids:
432 if not restore_numeric_ids and self.gid != 0:
433 # The grp might not exist on the local system.
434 grps = filter(None, [grp_from_gid(x) for x in user_gids])
435 if self.group in [x.gr_name for x in grps]:
436 g = grp_from_name(self.group)
440 if uid != -1 or gid != -1:
442 os.lchown(path, uid, gid)
444 if e.errno == errno.EPERM:
445 add_error('lchown: %s' % e)
446 elif sys.platform.startswith('cygwin') \
447 and e.errno == errno.EINVAL:
448 add_error('lchown: unknown uid/gid (%d/%d) for %s'
449 % (uid, gid, path_msg(path)))
455 os.lchmod(path, stat.S_IMODE(self.mode))
456 except errno.ENOSYS: # Function not implemented
458 elif not stat.S_ISLNK(self.mode):
459 os.chmod(path, stat.S_IMODE(self.mode))
464 def _encode_path(self):
466 return vint.pack('s', self.path)
470 def _load_path_rec(self, port):
471 self.path = vint.unpack('s', vint.read_bvec(port))[0]
476 def _add_symlink_target(self, path, st):
478 if stat.S_ISLNK(st.st_mode):
479 self.symlink_target = os.readlink(path)
480 # might have read a different link than the
481 # one that was in place when we did stat()
482 self.size = len(self.symlink_target)
484 add_error('readlink: %s' % e)
486 def _encode_symlink_target(self):
487 return self.symlink_target
489 def _load_symlink_target_rec(self, port):
490 target = vint.read_bvec(port)
491 self.symlink_target = target
492 if self.size is None:
493 self.size = len(target)
495 assert(self.size == len(target))
500 def _add_hardlink_target(self, target):
501 self.hardlink_target = target
503 def _same_hardlink_target(self, other):
504 """Return true or false to indicate similarity in the hardlink sense."""
505 return self.hardlink_target == other.hardlink_target
507 def _encode_hardlink_target(self):
508 return self.hardlink_target
510 def _load_hardlink_target_rec(self, port):
511 self.hardlink_target = vint.read_bvec(port)
514 ## POSIX1e ACL records
516 # Recorded as a list:
517 # [txt_id_acl, num_id_acl]
518 # or, if a directory:
519 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
520 # The numeric/text distinction only matters when reading/restoring
522 def _add_posix1e_acl(self, path, st):
525 if not stat.S_ISLNK(st.st_mode):
526 isdir = 1 if stat.S_ISDIR(st.st_mode) else 0
527 self.posix1e_acl = read_acl(path, isdir)
529 def _same_posix1e_acl(self, other):
530 """Return true or false to indicate similarity in the hardlink sense."""
531 return self.posix1e_acl == other.posix1e_acl
533 def _encode_posix1e_acl(self):
534 # Encode as two strings (w/default ACL string possibly empty).
536 acls = self.posix1e_acl
538 return vint.pack('ssss', acls[0], acls[1], b'', b'')
539 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
543 def _load_posix1e_acl_rec(self, port):
544 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
545 if acl_rep[2] == b'':
546 acl_rep = acl_rep[:2]
547 self.posix1e_acl = acl_rep
549 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
550 if not self.posix1e_acl:
554 add_error("%s: can't restore ACLs; posix1e support missing.\n"
559 acls = self.posix1e_acl
560 offs = 1 if restore_numeric_ids else 0
562 apply_acl(path, acls[offs], acls[offs + 2])
564 apply_acl(path, acls[offs])
566 if e.errno == errno.EINVAL:
567 # libacl returns with errno set to EINVAL if a user
568 # (or group) doesn't exist
569 raise ApplyError("POSIX1e ACL: can't create %r for %r"
570 % (acls, path_msg(path)))
571 elif e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
572 raise ApplyError('POSIX1e ACL applyto: %s' % e)
577 ## Linux attributes (lsattr(1), chattr(1))
579 def _add_linux_attr(self, path, st):
580 check_linux_file_attr_api()
581 if not get_linux_file_attr: return
582 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
584 attr = get_linux_file_attr(path)
586 self.linux_attr = attr
588 if e.errno == errno.EACCES:
589 add_error('read Linux attr: %s' % e)
590 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
591 # Assume filesystem doesn't support attrs.
593 elif e.errno == EINVAL:
594 global _warned_about_attr_einval
595 if not _warned_about_attr_einval:
596 log("Ignoring attr EINVAL;"
597 + " if you're not using ntfs-3g, please report: "
598 + path_msg(path) + '\n')
599 _warned_about_attr_einval = True
604 def _same_linux_attr(self, other):
605 """Return true or false to indicate similarity in the hardlink sense."""
606 return self.linux_attr == other.linux_attr
608 def _encode_linux_attr(self):
610 return vint.pack('V', self.linux_attr)
614 def _load_linux_attr_rec(self, port):
615 data = vint.read_bvec(port)
616 self.linux_attr = vint.unpack('V', data)[0]
618 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
620 check_linux_file_attr_api()
621 if not set_linux_file_attr:
622 add_error("%s: can't restore linuxattrs: "
623 "linuxattr support missing.\n" % path_msg(path))
626 set_linux_file_attr(path, self.linux_attr)
628 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
629 raise ApplyError('Linux chattr: %s (0x%s)'
630 % (e, hex(self.linux_attr)))
631 elif e.errno == EINVAL:
632 msg = "if you're not using ntfs-3g, please report"
633 raise ApplyError('Linux chattr: %s (0x%s) (%s)'
634 % (e, hex(self.linux_attr), msg))
639 ## Linux extended attributes (getfattr(1), setfattr(1))
641 def _add_linux_xattr(self, path, st):
644 self.linux_xattr = xattr.get_all(path, nofollow=True)
645 except EnvironmentError as e:
646 if e.errno != errno.EOPNOTSUPP:
649 def _same_linux_xattr(self, other):
650 """Return true or false to indicate similarity in the hardlink sense."""
651 return self.linux_xattr == other.linux_xattr
653 def _encode_linux_xattr(self):
655 result = vint.pack('V', len(self.linux_xattr))
656 for name, value in self.linux_xattr:
657 result += vint.pack('ss', name, value)
662 def _load_linux_xattr_rec(self, file):
663 data = vint.read_bvec(file)
664 memfile = BytesIO(data)
666 for i in range(vint.read_vuint(memfile)):
667 key = vint.read_bvec(memfile)
668 value = vint.read_bvec(memfile)
669 result.append((key, value))
670 self.linux_xattr = result
672 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
675 add_error("%s: can't restore xattr; xattr support missing.\n"
678 if not self.linux_xattr:
681 existing_xattrs = set(xattr.list(path, nofollow=True))
683 if e.errno == errno.EACCES:
684 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
687 for k, v in self.linux_xattr:
688 if k not in existing_xattrs \
689 or v != xattr.get(path, k, nofollow=True):
691 xattr.set(path, k, v, nofollow=True)
693 if e.errno == errno.EPERM \
694 or e.errno == errno.EOPNOTSUPP:
695 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
698 existing_xattrs -= frozenset([k])
699 for k in existing_xattrs:
701 xattr.remove(path, k, nofollow=True)
703 if e.errno in (errno.EPERM, errno.EACCES):
704 raise ApplyError('xattr.remove %r: %s' % (path_msg(path), e))
709 self.mode = self.uid = self.gid = self.user = self.group = None
710 self.atime = self.mtime = self.ctime = None
714 self.symlink_target = None
715 self.hardlink_target = None
716 self.linux_attr = None
717 self.linux_xattr = None
718 self.posix1e_acl = None
720 def __eq__(self, other):
721 if not isinstance(other, Metadata): return False
722 if self.mode != other.mode: return False
723 if self.mtime != other.mtime: return False
724 if self.ctime != other.ctime: return False
725 if self.atime != other.atime: return False
726 if self.path != other.path: return False
727 if self.uid != other.uid: return False
728 if self.gid != other.gid: return False
729 if self.size != other.size: return False
730 if self.user != other.user: return False
731 if self.group != other.group: return False
732 if self.symlink_target != other.symlink_target: return False
733 if self.hardlink_target != other.hardlink_target: return False
734 if self.linux_attr != other.linux_attr: return False
735 if self.posix1e_acl != other.posix1e_acl: return False
738 def __ne__(self, other):
739 return not self.__eq__(other)
742 return hash((self.mode,
753 self.hardlink_target,
758 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
759 if self.path is not None:
760 result += ' path:' + repr(self.path)
761 if self.mode is not None:
762 result += ' mode: %o (%s)' % (self.mode, xstat.mode_str(self.mode))
763 if self.uid is not None:
764 result += ' uid:' + str(self.uid)
765 if self.gid is not None:
766 result += ' gid:' + str(self.gid)
767 if self.user is not None:
768 result += ' user:' + repr(self.user)
769 if self.group is not None:
770 result += ' group:' + repr(self.group)
771 if self.size is not None:
772 result += ' size:' + repr(self.size)
773 for name, val in (('atime', self.atime),
774 ('mtime', self.mtime),
775 ('ctime', self.ctime)):
777 result += ' %s:%r (%d)' \
779 strftime('%Y-%m-%d %H:%M %z',
780 gmtime(xstat.fstime_floor_secs(val))),
783 return ''.join(result)
785 def write(self, port, include_path=True):
786 port.write(self.encode(include_path=include_path))
788 def encode(self, include_path=True):
790 records = include_path and [(_rec_tag_path, self._encode_path())] or []
791 records.extend([(_rec_tag_common_v3, self._encode_common()),
792 (_rec_tag_symlink_target,
793 self._encode_symlink_target()),
794 (_rec_tag_hardlink_target,
795 self._encode_hardlink_target()),
796 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
797 (_rec_tag_linux_attr, self._encode_linux_attr()),
798 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
799 for tag, data in records:
801 ret.extend((vint.encode_vuint(tag),
802 vint.encode_bvec(data)))
803 ret.append(vint.encode_vuint(_rec_tag_end))
807 return deepcopy(self)
811 # This method should either return a valid Metadata object,
812 # return None if there was no information at all (just a
813 # _rec_tag_end), throw EOFError if there was nothing at all to
814 # read, or throw an Exception if a valid object could not be
816 tag = vint.read_vuint(port)
817 if tag == _rec_tag_end:
819 try: # From here on, EOF is an error.
821 while True: # only exit is error (exception) or _rec_tag_end
822 if tag == _rec_tag_path:
823 result._load_path_rec(port)
824 elif tag == _rec_tag_common_v3:
825 result._load_common_rec(port, version=3)
826 elif tag == _rec_tag_common_v2:
827 result._load_common_rec(port, version=2)
828 elif tag == _rec_tag_symlink_target:
829 result._load_symlink_target_rec(port)
830 elif tag == _rec_tag_hardlink_target:
831 result._load_hardlink_target_rec(port)
832 elif tag == _rec_tag_posix1e_acl:
833 result._load_posix1e_acl_rec(port)
834 elif tag == _rec_tag_linux_attr:
835 result._load_linux_attr_rec(port)
836 elif tag == _rec_tag_linux_xattr:
837 result._load_linux_xattr_rec(port)
838 elif tag == _rec_tag_end:
840 elif tag == _rec_tag_common_v1: # Should be very rare.
841 result._load_common_rec(port, version=1)
842 else: # unknown record
844 tag = vint.read_vuint(port)
846 raise Exception("EOF while reading Metadata")
849 return stat.S_ISDIR(self.mode)
851 def create_path(self, path, create_symlinks=True):
852 self._create_via_common_rec(path, create_symlinks=create_symlinks)
854 def apply_to_path(self, path=None, restore_numeric_ids=False):
855 # apply metadata to path -- file must exist
859 raise Exception('Metadata.apply_to_path() called with no path')
860 if not self._recognized_file_type():
861 add_error('not applying metadata to "%s"' % path_msg(path)
862 + ' with unrecognized mode "0x%x"\n' % self.mode)
864 num_ids = restore_numeric_ids
865 for apply_metadata in (self._apply_common_rec,
866 self._apply_posix1e_acl_rec,
867 self._apply_linux_attr_rec,
868 self._apply_linux_xattr_rec):
870 apply_metadata(path, restore_numeric_ids=num_ids)
871 except ApplyError as e:
874 def same_file(self, other):
875 """Compare this to other for equivalency. Return true if
876 their information implies they could represent the same file
877 on disk, in the hardlink sense. Assume they're both regular
879 return self._same_common(other) \
880 and self._same_hardlink_target(other) \
881 and self._same_posix1e_acl(other) \
882 and self._same_linux_attr(other) \
883 and self._same_linux_xattr(other)
886 def from_path(path, statinfo=None, archive_path=None,
887 save_symlinks=True, hardlink_target=None,
888 normalized=False, after_stat=None):
889 # This function is also a test hook; see test-save-errors
890 """Return the metadata associated with the path. When normalized is
891 true, return the metadata appropriate for a typical save, which
892 may or may not be all of it."""
894 result.path = archive_path
895 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)