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, pwd, grp, 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.pwdgrp import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
19 from bup.xstat import utime, lutime
22 if sys.platform.startswith('linux'):
23 # prefer python-pyxattr (it's a lot faster), but fall back to python-xattr
24 # as the two are incompatible and only one can be installed on a system
28 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
29 if xattr and getattr(xattr, 'get_all', None) is None:
31 from xattr import pyxattr_compat as xattr
33 log('Warning: python-xattr module is too old; '
34 'upgrade or install python-pyxattr instead.\n')
38 if not (sys.platform.startswith('cygwin') \
39 or sys.platform.startswith('darwin') \
40 or sys.platform.startswith('netbsd')):
44 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
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('/../')
127 result = result[result.rfind('/../') + 4:]
129 # Take everything after any remaining '../'.
130 if result.startswith("../"):
133 # Remove any '/./' sequences.
134 pos = result.find('/./')
136 result = result[0:pos] + '/' + result[pos + 3:]
137 pos = result.find('/./')
139 # Remove any leading '/'s.
140 result = result.lstrip('/')
142 # Replace '//' with '/' everywhere.
143 pos = result.find('//')
145 result = result[0:pos] + '/' + result[pos + 2:]
146 pos = result.find('//')
148 # Take everything after any remaining './'.
149 if result.startswith('./'):
152 # Take everything before any remaining '/.'.
153 if result.endswith('/.'):
156 if result == '' or result.endswith('/..'):
163 if p.startswith('/'):
165 if p.find('/../') != -1:
167 if p.startswith('../'):
169 if p.endswith('/..'):
174 def _clean_up_extract_path(p):
175 result = p.lstrip('/')
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 = ''
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 ' + path)
327 # If the path already exists and is a dir, try rmdir.
328 # If the path already exists and is anything else, try unlink.
331 st = xstat.lstat(path)
333 if e.errno != errno.ENOENT:
336 if stat.S_ISDIR(st.st_mode):
340 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
341 msg = 'refusing to overwrite non-empty dir ' + path
347 if stat.S_ISREG(self.mode):
348 assert(self._recognized_file_type())
349 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
351 elif stat.S_ISDIR(self.mode):
352 assert(self._recognized_file_type())
353 os.mkdir(path, 0o700)
354 elif stat.S_ISCHR(self.mode):
355 assert(self._recognized_file_type())
356 os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
357 elif stat.S_ISBLK(self.mode):
358 assert(self._recognized_file_type())
359 os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
360 elif stat.S_ISFIFO(self.mode):
361 assert(self._recognized_file_type())
362 os.mkfifo(path, 0o600 | stat.S_IFIFO)
363 elif stat.S_ISSOCK(self.mode):
365 os.mknod(path, 0o600 | stat.S_IFSOCK)
367 if e.errno in (errno.EINVAL, errno.EPERM):
368 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
372 elif stat.S_ISLNK(self.mode):
373 assert(self._recognized_file_type())
374 if self.symlink_target and create_symlinks:
375 # on MacOS, symlink() permissions depend on umask, and there's
376 # no way to chown a symlink after creating it, so we have to
378 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
380 os.symlink(self.symlink_target, path)
383 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
385 assert(not self._recognized_file_type())
386 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
389 def _apply_common_rec(self, path, restore_numeric_ids=False):
391 raise ApplyError('no metadata - cannot apply to ' + path)
393 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
394 # EACCES errors at this stage are fatal for the current path.
395 if lutime and stat.S_ISLNK(self.mode):
397 lutime(path, (self.atime, self.mtime))
399 if e.errno == errno.EACCES:
400 raise ApplyError('lutime: %s' % e)
405 utime(path, (self.atime, self.mtime))
407 if e.errno == errno.EACCES:
408 raise ApplyError('utime: %s' % e)
412 uid = gid = -1 # By default, do nothing.
416 if not restore_numeric_ids:
417 if self.uid != 0 and self.user:
418 entry = pwd_from_name(self.user)
421 if self.gid != 0 and self.group:
422 entry = grp_from_name(self.group)
425 else: # not superuser - only consider changing the group/gid
426 user_gids = os.getgroups()
427 if self.gid in user_gids:
429 if not restore_numeric_ids and self.gid != 0:
430 # The grp might not exist on the local system.
431 grps = filter(None, [grp_from_gid(x) for x in user_gids])
432 if self.group in [x.gr_name for x in grps]:
433 g = grp_from_name(self.group)
437 if uid != -1 or gid != -1:
439 os.lchown(path, uid, gid)
441 if e.errno == errno.EPERM:
442 add_error('lchown: %s' % e)
443 elif sys.platform.startswith('cygwin') \
444 and e.errno == errno.EINVAL:
445 add_error('lchown: unknown uid/gid (%d/%d) for %s'
452 os.lchmod(path, stat.S_IMODE(self.mode))
453 except errno.ENOSYS: # Function not implemented
455 elif not stat.S_ISLNK(self.mode):
456 os.chmod(path, stat.S_IMODE(self.mode))
461 def _encode_path(self):
463 return vint.pack('s', self.path)
467 def _load_path_rec(self, port):
468 self.path = vint.unpack('s', vint.read_bvec(port))[0]
473 def _add_symlink_target(self, path, st):
475 if stat.S_ISLNK(st.st_mode):
476 self.symlink_target = os.readlink(path)
478 add_error('readlink: %s' % e)
480 def _encode_symlink_target(self):
481 return self.symlink_target
483 def _load_symlink_target_rec(self, port):
484 target = vint.read_bvec(port)
485 self.symlink_target = target
486 if self.size is None:
487 self.size = len(target)
489 assert(self.size == len(target))
494 def _add_hardlink_target(self, target):
495 self.hardlink_target = target
497 def _same_hardlink_target(self, other):
498 """Return true or false to indicate similarity in the hardlink sense."""
499 return self.hardlink_target == other.hardlink_target
501 def _encode_hardlink_target(self):
502 return self.hardlink_target
504 def _load_hardlink_target_rec(self, port):
505 self.hardlink_target = vint.read_bvec(port)
508 ## POSIX1e ACL records
510 # Recorded as a list:
511 # [txt_id_acl, num_id_acl]
512 # or, if a directory:
513 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
514 # The numeric/text distinction only matters when reading/restoring
516 def _add_posix1e_acl(self, path, st):
517 if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
519 if not stat.S_ISLNK(st.st_mode):
523 if posix1e.has_extended(path):
524 acl = posix1e.ACL(file=path)
525 acls = [acl, acl] # txt and num are the same
526 if stat.S_ISDIR(st.st_mode):
527 def_acl = posix1e.ACL(filedef=path)
528 def_acls = [def_acl, def_acl]
529 except EnvironmentError as e:
530 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
533 txt_flags = posix1e.TEXT_ABBREVIATE
534 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
535 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
536 acls[1].to_any_text('', '\n', num_flags)]
538 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
539 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
540 self.posix1e_acl = acl_rep
542 def _same_posix1e_acl(self, other):
543 """Return true or false to indicate similarity in the hardlink sense."""
544 return self.posix1e_acl == other.posix1e_acl
546 def _encode_posix1e_acl(self):
547 # Encode as two strings (w/default ACL string possibly empty).
549 acls = self.posix1e_acl
551 acls.extend(['', ''])
552 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
556 def _load_posix1e_acl_rec(self, port):
557 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
559 acl_rep = acl_rep[:2]
560 self.posix1e_acl = acl_rep
562 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
563 def apply_acl(acl_rep, kind):
565 acl = posix1e.ACL(text = acl_rep)
568 # pylibacl appears to return an IOError with errno
569 # set to 0 if a group referred to by the ACL rep
570 # doesn't exist on the current system.
571 raise ApplyError("POSIX1e ACL: can't create %r for %r"
576 acl.applyto(path, kind)
578 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
579 raise ApplyError('POSIX1e ACL applyto: %s' % e)
585 add_error("%s: can't restore ACLs; posix1e support missing.\n"
589 acls = self.posix1e_acl
591 if restore_numeric_ids:
592 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
594 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
595 if restore_numeric_ids:
596 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
598 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
601 ## Linux attributes (lsattr(1), chattr(1))
603 def _add_linux_attr(self, path, st):
604 check_linux_file_attr_api()
605 if not get_linux_file_attr: return
606 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
608 attr = get_linux_file_attr(path)
610 self.linux_attr = attr
612 if e.errno == errno.EACCES:
613 add_error('read Linux attr: %s' % e)
614 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
615 # Assume filesystem doesn't support attrs.
617 elif e.errno == EINVAL:
618 global _warned_about_attr_einval
619 if not _warned_about_attr_einval:
620 log("Ignoring attr EINVAL;"
621 + " if you're not using ntfs-3g, please report: "
623 _warned_about_attr_einval = True
628 def _same_linux_attr(self, other):
629 """Return true or false to indicate similarity in the hardlink sense."""
630 return self.linux_attr == other.linux_attr
632 def _encode_linux_attr(self):
634 return vint.pack('V', self.linux_attr)
638 def _load_linux_attr_rec(self, port):
639 data = vint.read_bvec(port)
640 self.linux_attr = vint.unpack('V', data)[0]
642 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
644 check_linux_file_attr_api()
645 if not set_linux_file_attr:
646 add_error("%s: can't restore linuxattrs: "
647 "linuxattr support missing.\n" % path)
650 set_linux_file_attr(path, self.linux_attr)
652 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
653 raise ApplyError('Linux chattr: %s (0x%s)'
654 % (e, hex(self.linux_attr)))
655 elif e.errno == EINVAL:
656 msg = "if you're not using ntfs-3g, please report"
657 raise ApplyError('Linux chattr: %s (0x%s) (%s)'
658 % (e, hex(self.linux_attr), msg))
663 ## Linux extended attributes (getfattr(1), setfattr(1))
665 def _add_linux_xattr(self, path, st):
668 self.linux_xattr = xattr.get_all(path, nofollow=True)
669 except EnvironmentError as e:
670 if e.errno != errno.EOPNOTSUPP:
673 def _same_linux_xattr(self, other):
674 """Return true or false to indicate similarity in the hardlink sense."""
675 return self.linux_xattr == other.linux_xattr
677 def _encode_linux_xattr(self):
679 result = vint.pack('V', len(self.linux_xattr))
680 for name, value in self.linux_xattr:
681 result += vint.pack('ss', name, value)
686 def _load_linux_xattr_rec(self, file):
687 data = vint.read_bvec(file)
688 memfile = BytesIO(data)
690 for i in range(vint.read_vuint(memfile)):
691 key = vint.read_bvec(memfile)
692 value = vint.read_bvec(memfile)
693 result.append((key, value))
694 self.linux_xattr = result
696 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
699 add_error("%s: can't restore xattr; xattr support missing.\n"
702 if not self.linux_xattr:
705 existing_xattrs = set(xattr.list(path, nofollow=True))
707 if e.errno == errno.EACCES:
708 raise ApplyError('xattr.set %r: %s' % (path, e))
711 for k, v in self.linux_xattr:
712 if k not in existing_xattrs \
713 or v != xattr.get(path, k, nofollow=True):
715 xattr.set(path, k, v, nofollow=True)
717 if e.errno == errno.EPERM \
718 or e.errno == errno.EOPNOTSUPP:
719 raise ApplyError('xattr.set %r: %s' % (path, e))
722 existing_xattrs -= frozenset([k])
723 for k in existing_xattrs:
725 xattr.remove(path, k, nofollow=True)
727 if e.errno in (errno.EPERM, errno.EACCES):
728 raise ApplyError('xattr.remove %r: %s' % (path, e))
733 self.mode = self.uid = self.gid = self.user = self.group = None
734 self.atime = self.mtime = self.ctime = None
738 self.symlink_target = None
739 self.hardlink_target = None
740 self.linux_attr = None
741 self.linux_xattr = None
742 self.posix1e_acl = None
744 def __eq__(self, other):
745 if not isinstance(other, Metadata): return False
746 if self.mode != other.mode: return False
747 if self.mtime != other.mtime: return False
748 if self.ctime != other.ctime: return False
749 if self.atime != other.atime: return False
750 if self.path != other.path: return False
751 if self.uid != other.uid: return False
752 if self.gid != other.gid: return False
753 if self.size != other.size: return False
754 if self.user != other.user: return False
755 if self.group != other.group: return False
756 if self.symlink_target != other.symlink_target: return False
757 if self.hardlink_target != other.hardlink_target: return False
758 if self.linux_attr != other.linux_attr: return False
759 if self.posix1e_acl != other.posix1e_acl: return False
762 def __ne__(self, other):
763 return not self.__eq__(other)
766 return hash((self.mode,
777 self.hardlink_target,
782 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
783 if self.path is not None:
784 result += ' path:' + repr(self.path)
785 if self.mode is not None:
786 result += ' mode:' + repr(xstat.mode_str(self.mode)
787 + '(%s)' % oct(self.mode))
788 if self.uid is not None:
789 result += ' uid:' + str(self.uid)
790 if self.gid is not None:
791 result += ' gid:' + str(self.gid)
792 if self.user is not None:
793 result += ' user:' + repr(self.user)
794 if self.group is not None:
795 result += ' group:' + repr(self.group)
796 if self.size is not None:
797 result += ' size:' + repr(self.size)
798 for name, val in (('atime', self.atime),
799 ('mtime', self.mtime),
800 ('ctime', self.ctime)):
802 result += ' %s:%r (%d)' \
804 strftime('%Y-%m-%d %H:%M %z',
805 gmtime(xstat.fstime_floor_secs(val))),
808 return ''.join(result)
810 def write(self, port, include_path=True):
811 records = include_path and [(_rec_tag_path, self._encode_path())] or []
812 records.extend([(_rec_tag_common_v3, self._encode_common()),
813 (_rec_tag_symlink_target,
814 self._encode_symlink_target()),
815 (_rec_tag_hardlink_target,
816 self._encode_hardlink_target()),
817 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
818 (_rec_tag_linux_attr, self._encode_linux_attr()),
819 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
820 for tag, data in records:
822 vint.write_vuint(port, tag)
823 vint.write_bvec(port, data)
824 vint.write_vuint(port, _rec_tag_end)
826 def encode(self, include_path=True):
828 self.write(port, include_path)
829 return port.getvalue()
832 return deepcopy(self)
836 # This method should either return a valid Metadata object,
837 # return None if there was no information at all (just a
838 # _rec_tag_end), throw EOFError if there was nothing at all to
839 # read, or throw an Exception if a valid object could not be
841 tag = vint.read_vuint(port)
842 if tag == _rec_tag_end:
844 try: # From here on, EOF is an error.
846 while True: # only exit is error (exception) or _rec_tag_end
847 if tag == _rec_tag_path:
848 result._load_path_rec(port)
849 elif tag == _rec_tag_common_v3:
850 result._load_common_rec(port, version=3)
851 elif tag == _rec_tag_common_v2:
852 result._load_common_rec(port, version=2)
853 elif tag == _rec_tag_symlink_target:
854 result._load_symlink_target_rec(port)
855 elif tag == _rec_tag_hardlink_target:
856 result._load_hardlink_target_rec(port)
857 elif tag == _rec_tag_posix1e_acl:
858 result._load_posix1e_acl_rec(port)
859 elif tag == _rec_tag_linux_attr:
860 result._load_linux_attr_rec(port)
861 elif tag == _rec_tag_linux_xattr:
862 result._load_linux_xattr_rec(port)
863 elif tag == _rec_tag_end:
865 elif tag == _rec_tag_common_v1: # Should be very rare.
866 result._load_common_rec(port, version=1)
867 else: # unknown record
869 tag = vint.read_vuint(port)
871 raise Exception("EOF while reading Metadata")
874 return stat.S_ISDIR(self.mode)
876 def create_path(self, path, create_symlinks=True):
877 self._create_via_common_rec(path, create_symlinks=create_symlinks)
879 def apply_to_path(self, path=None, restore_numeric_ids=False):
880 # apply metadata to path -- file must exist
884 raise Exception('Metadata.apply_to_path() called with no path')
885 if not self._recognized_file_type():
886 add_error('not applying metadata to "%s"' % path
887 + ' with unrecognized mode "0x%x"\n' % self.mode)
889 num_ids = restore_numeric_ids
890 for apply_metadata in (self._apply_common_rec,
891 self._apply_posix1e_acl_rec,
892 self._apply_linux_attr_rec,
893 self._apply_linux_xattr_rec):
895 apply_metadata(path, restore_numeric_ids=num_ids)
896 except ApplyError as e:
899 def same_file(self, other):
900 """Compare this to other for equivalency. Return true if
901 their information implies they could represent the same file
902 on disk, in the hardlink sense. Assume they're both regular
904 return self._same_common(other) \
905 and self._same_hardlink_target(other) \
906 and self._same_posix1e_acl(other) \
907 and self._same_linux_attr(other) \
908 and self._same_linux_xattr(other)
911 def from_path(path, statinfo=None, archive_path=None,
912 save_symlinks=True, hardlink_target=None,
914 """Return the metadata associated with the path. When normalized is
915 true, return the metadata appropriate for a typical save, which
916 may or may not be all of it."""
918 result.path = archive_path
919 st = statinfo or xstat.lstat(path)
920 result._add_common(path, st)
922 result._add_symlink_target(path, st)
923 result._add_hardlink_target(hardlink_target)
924 result._add_posix1e_acl(path, st)
925 result._add_linux_attr(path, st)
926 result._add_linux_xattr(path, st)
928 # Only store sizes for regular files and symlinks for now.
929 if not (stat.S_ISREG(result.mode) or stat.S_ISLNK(result.mode)):
934 def save_tree(output_file, paths,
940 # Issue top-level rewrite warnings.
942 safe_path = _clean_up_path_for_archive(path)
943 if safe_path != path:
944 log('archiving "%s" as "%s"\n' % (path, safe_path))
948 safe_path = _clean_up_path_for_archive(p)
950 if stat.S_ISDIR(st.st_mode):
952 m = from_path(p, statinfo=st, archive_path=safe_path,
953 save_symlinks=save_symlinks)
955 print(m.path, file=sys.stderr)
956 m.write(output_file, include_path=write_paths)
958 start_dir = os.getcwd()
960 for (p, st) in recursive_dirlist(paths, xdev=xdev):
961 dirlist_dir = os.getcwd()
963 safe_path = _clean_up_path_for_archive(p)
964 m = from_path(p, statinfo=st, archive_path=safe_path,
965 save_symlinks=save_symlinks)
967 print(m.path, file=sys.stderr)
968 m.write(output_file, include_path=write_paths)
969 os.chdir(dirlist_dir)
974 def _set_up_path(meta, create_symlinks=True):
975 # Allow directories to exist as a special case -- might have
976 # been created by an earlier longer path.
980 parent = os.path.dirname(meta.path)
983 meta.create_path(meta.path, create_symlinks=create_symlinks)
986 all_fields = frozenset(['path',
1003 def summary_str(meta, numeric_ids = False, classification = None,
1004 human_readable = False):
1006 """Return a string containing the "ls -l" style listing for meta.
1007 Classification may be "all", "type", or None."""
1008 user_str = group_str = size_or_dev_str = '?'
1009 symlink_target = None
1012 mode_str = xstat.mode_str(meta.mode)
1013 symlink_target = meta.symlink_target
1014 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
1015 mtime_str = strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
1016 if meta.user and not numeric_ids:
1017 user_str = meta.user
1018 elif meta.uid != None:
1019 user_str = str(meta.uid)
1020 if meta.group and not numeric_ids:
1021 group_str = meta.group
1022 elif meta.gid != None:
1023 group_str = str(meta.gid)
1024 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
1026 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
1027 os.minor(meta.rdev))
1028 elif meta.size != None:
1030 size_or_dev_str = format_filesize(meta.size)
1032 size_or_dev_str = str(meta.size)
1034 size_or_dev_str = '-'
1036 classification_str = \
1037 xstat.classification_str(meta.mode, classification == 'all')
1040 mtime_str = '????-??-?? ??:??'
1041 classification_str = '?'
1045 name += classification_str
1047 name += ' -> ' + meta.symlink_target
1049 return '%-10s %-11s %11s %16s %s' % (mode_str,
1050 user_str + "/" + group_str,
1056 def detailed_str(meta, fields = None):
1057 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
1058 # 0", "link-target:", etc.
1063 if 'path' in fields:
1064 path = meta.path or ''
1065 result.append('path: ' + path)
1066 if 'mode' in fields:
1067 result.append('mode: %s (%s)' % (oct(meta.mode),
1068 xstat.mode_str(meta.mode)))
1069 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1070 result.append('link-target: ' + meta.symlink_target)
1071 if 'rdev' in fields:
1073 result.append('rdev: %d,%d' % (os.major(meta.rdev),
1074 os.minor(meta.rdev)))
1076 result.append('rdev: 0')
1077 if 'size' in fields and meta.size is not None:
1078 result.append('size: ' + str(meta.size))
1080 result.append('uid: ' + str(meta.uid))
1082 result.append('gid: ' + str(meta.gid))
1083 if 'user' in fields:
1084 result.append('user: ' + meta.user)
1085 if 'group' in fields:
1086 result.append('group: ' + meta.group)
1087 if 'atime' in fields:
1088 # If we don't have xstat.lutime, that means we have to use
1089 # utime(), and utime() has no way to set the mtime/atime of a
1090 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1091 # so let's not report it. (That way scripts comparing
1092 # before/after won't trigger.)
1093 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1094 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1096 result.append('atime: 0')
1097 if 'mtime' in fields:
1098 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1099 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1101 result.append('mtime: 0')
1102 if 'ctime' in fields:
1103 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1104 if 'linux-attr' in fields and meta.linux_attr:
1105 result.append('linux-attr: ' + hex(meta.linux_attr))
1106 if 'linux-xattr' in fields and meta.linux_xattr:
1107 for name, value in meta.linux_xattr:
1108 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1109 if 'posix1e-acl' in fields and meta.posix1e_acl:
1110 acl = meta.posix1e_acl[0]
1111 result.append('posix1e-acl: ' + acl + '\n')
1112 if stat.S_ISDIR(meta.mode):
1113 def_acl = meta.posix1e_acl[2]
1114 result.append('posix1e-acl-default: ' + def_acl + '\n')
1115 return '\n'.join(result)
1118 class _ArchiveIterator:
1121 return Metadata.read(self._file)
1123 raise StopIteration()
1128 def __init__(self, file):
1132 def display_archive(file):
1135 for meta in _ArchiveIterator(file):
1138 print(detailed_str(meta))
1141 for meta in _ArchiveIterator(file):
1142 print(summary_str(meta))
1144 for meta in _ArchiveIterator(file):
1146 print('bup: no metadata path, but asked to only display path'
1147 '(increase verbosity?)')
1152 def start_extract(file, create_symlinks=True):
1153 for meta in _ArchiveIterator(file):
1154 if not meta: # Hit end record.
1157 print(meta.path, file=sys.stderr)
1158 xpath = _clean_up_extract_path(meta.path)
1160 add_error(Exception('skipping risky path "%s"' % meta.path))
1163 _set_up_path(meta, create_symlinks=create_symlinks)
1166 def finish_extract(file, restore_numeric_ids=False):
1168 for meta in _ArchiveIterator(file):
1169 if not meta: # Hit end record.
1171 xpath = _clean_up_extract_path(meta.path)
1173 add_error(Exception('skipping risky path "%s"' % dir.path))
1175 if os.path.isdir(meta.path):
1176 all_dirs.append(meta)
1179 print(meta.path, file=sys.stderr)
1180 meta.apply_to_path(path=xpath,
1181 restore_numeric_ids=restore_numeric_ids)
1182 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1183 for dir in all_dirs:
1184 # Don't need to check xpath -- won't be in all_dirs if not OK.
1185 xpath = _clean_up_extract_path(dir.path)
1187 print(dir.path, file=sys.stderr)
1188 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1191 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1192 # For now, just store all the directories and handle them last,
1195 for meta in _ArchiveIterator(file):
1196 if not meta: # Hit end record.
1198 xpath = _clean_up_extract_path(meta.path)
1200 add_error(Exception('skipping risky path "%s"' % meta.path))
1204 print('+', meta.path, file=sys.stderr)
1205 _set_up_path(meta, create_symlinks=create_symlinks)
1206 if os.path.isdir(meta.path):
1207 all_dirs.append(meta)
1210 print('=', meta.path, file=sys.stderr)
1211 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1212 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1213 for dir in all_dirs:
1214 # Don't need to check xpath -- won't be in all_dirs if not OK.
1215 xpath = _clean_up_extract_path(dir.path)
1217 print('=', xpath, file=sys.stderr)
1218 # Shouldn't have to check for risky paths here (omitted above).
1219 dir.apply_to_path(path=dir.path,
1220 restore_numeric_ids=restore_numeric_ids)