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
32 if not isinstance(xattr.NS_USER, bytes):
37 log('Warning: python-xattr module is too old; '
38 'upgrade or install python-pyxattr instead.\n')
41 if not (sys.platform.startswith('cygwin') \
42 or sys.platform.startswith('darwin') \
43 or sys.platform.startswith('netbsd')):
47 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
50 from bup._helpers import get_linux_file_attr, set_linux_file_attr
52 # No need for a warning here; the only reason they won't exist is that we're
53 # not on Linux, in which case files don't have any linux attrs anyway, so
54 # lacking the functions isn't a problem.
55 get_linux_file_attr = set_linux_file_attr = None
58 # See the bup_get_linux_file_attr() comments.
59 _suppress_linux_file_attr = \
60 sys.byteorder == 'big' and struct.calcsize('@l') > struct.calcsize('@i')
62 def check_linux_file_attr_api():
63 global get_linux_file_attr, set_linux_file_attr
64 if not (get_linux_file_attr or set_linux_file_attr):
66 if _suppress_linux_file_attr:
67 log('Warning: Linux attr support disabled (see "bup help index").\n')
68 get_linux_file_attr = set_linux_file_attr = None
71 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
73 # Q: Consider hardlink support?
74 # Q: Is it OK to store raw linux attr (chattr) flags?
75 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
76 # Q: Is the application of posix1e has_extended() correct?
77 # Q: Is one global --numeric-ids argument sufficient?
78 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
79 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
81 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
82 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
83 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
84 # FIXME: Consider pack('vvvvsss', ...) optimization.
88 # osx (varies between hfs and hfs+):
89 # type - regular dir char block fifo socket ...
90 # perms - rwxrwxrwxsgt
91 # times - ctime atime mtime
94 # hard-link-info (hfs+ only)
97 # attributes-osx see chflags
103 # type - regular dir ...
104 # times - creation, modification, posix change, access
107 # attributes - see attrib
109 # forks (alternate data streams)
113 # type - regular dir ...
114 # perms - rwxrwxrwx (maybe - see wikipedia)
115 # times - creation, modification, access
116 # attributes - see attrib
120 _have_lchmod = hasattr(os, 'lchmod')
123 def _clean_up_path_for_archive(p):
124 # Not the most efficient approach.
127 # Take everything after any '/../'.
128 pos = result.rfind('/../')
130 result = result[result.rfind('/../') + 4:]
132 # Take everything after any remaining '../'.
133 if result.startswith("../"):
136 # Remove any '/./' sequences.
137 pos = result.find('/./')
139 result = result[0:pos] + '/' + result[pos + 3:]
140 pos = result.find('/./')
142 # Remove any leading '/'s.
143 result = result.lstrip('/')
145 # Replace '//' with '/' everywhere.
146 pos = result.find('//')
148 result = result[0:pos] + '/' + result[pos + 2:]
149 pos = result.find('//')
151 # Take everything after any remaining './'.
152 if result.startswith('./'):
155 # Take everything before any remaining '/.'.
156 if result.endswith('/.'):
159 if result == '' or result.endswith('/..'):
166 if p.startswith('/'):
168 if p.find('/../') != -1:
170 if p.startswith('../'):
172 if p.endswith('/..'):
177 def _clean_up_extract_path(p):
178 result = p.lstrip('/')
181 elif _risky_path(result):
187 # These tags are currently conceptually private to Metadata, and they
188 # must be unique, and must *never* be changed.
191 _rec_tag_common_v1 = 2 # times, user, group, type, perms, etc. (legacy/broken)
192 _rec_tag_symlink_target = 3
193 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
194 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
195 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
196 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
197 _rec_tag_hardlink_target = 8 # hard link target path
198 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
199 _rec_tag_common_v3 = 10 # adds optional size to v2
201 _warned_about_attr_einval = None
204 class ApplyError(Exception):
205 # Thrown when unable to apply any given bit of metadata to a path.
210 # Metadata is stored as a sequence of tagged binary records. Each
211 # record will have some subset of add, encode, load, create, and
212 # apply methods, i.e. _add_foo...
214 # We do allow an "empty" object as a special case, i.e. no
215 # records. One can be created by trying to write Metadata(), and
216 # for such an object, read() will return None. This is used by
217 # "bup save", for example, as a placeholder in cases where
220 # NOTE: if any relevant fields are added or removed, be sure to
221 # update same_file() below.
225 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
226 # must be non-negative and < 10**9.
228 def _add_common(self, path, st):
229 assert(st.st_uid >= 0)
230 assert(st.st_gid >= 0)
231 self.size = st.st_size
234 self.atime = st.st_atime
235 self.mtime = st.st_mtime
236 self.ctime = st.st_ctime
237 self.user = self.group = ''
238 entry = pwd_from_uid(st.st_uid)
240 self.user = entry.pw_name
241 entry = grp_from_gid(st.st_gid)
243 self.group = entry.gr_name
244 self.mode = st.st_mode
245 # Only collect st_rdev if we might need it for a mknod()
246 # during restore. On some platforms (i.e. kFreeBSD), it isn't
247 # stable for other file types. For example "cp -a" will
248 # change it for a plain file.
249 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
250 self.rdev = st.st_rdev
254 def _same_common(self, other):
255 """Return true or false to indicate similarity in the hardlink sense."""
256 return self.uid == other.uid \
257 and self.gid == other.gid \
258 and self.rdev == other.rdev \
259 and self.mtime == other.mtime \
260 and self.ctime == other.ctime \
261 and self.user == other.user \
262 and self.group == other.group \
263 and self.size == other.size
265 def _encode_common(self):
268 atime = xstat.nsecs_to_timespec(self.atime)
269 mtime = xstat.nsecs_to_timespec(self.mtime)
270 ctime = xstat.nsecs_to_timespec(self.ctime)
271 result = vint.pack('vvsvsvvVvVvVv',
284 self.size if self.size is not None else -1)
287 def _load_common_rec(self, port, version=3):
289 # Added trailing size to v2, negative when None.
290 unpack_fmt = 'vvsvsvvVvVvVv'
292 unpack_fmt = 'vvsvsvvVvVvV'
294 unpack_fmt = 'VVsVsVvVvVvV'
296 raise Exception('unexpected common_rec version %d' % version)
297 data = vint.read_bvec(port)
298 values = vint.unpack(unpack_fmt, data)
300 (self.mode, self.uid, self.user, self.gid, self.group,
302 self.atime, atime_ns,
303 self.mtime, mtime_ns,
304 self.ctime, ctime_ns, size) = values
308 (self.mode, self.uid, self.user, self.gid, self.group,
310 self.atime, atime_ns,
311 self.mtime, mtime_ns,
312 self.ctime, ctime_ns) = values
313 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
314 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
315 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
317 def _recognized_file_type(self):
318 return stat.S_ISREG(self.mode) \
319 or stat.S_ISDIR(self.mode) \
320 or stat.S_ISCHR(self.mode) \
321 or stat.S_ISBLK(self.mode) \
322 or stat.S_ISFIFO(self.mode) \
323 or stat.S_ISSOCK(self.mode) \
324 or stat.S_ISLNK(self.mode)
326 def _create_via_common_rec(self, path, create_symlinks=True):
328 raise ApplyError('no metadata - cannot create path ' + path)
330 # If the path already exists and is a dir, try rmdir.
331 # If the path already exists and is anything else, try unlink.
334 st = xstat.lstat(path)
336 if e.errno != errno.ENOENT:
339 if stat.S_ISDIR(st.st_mode):
343 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
344 msg = 'refusing to overwrite non-empty dir ' + path
350 if stat.S_ISREG(self.mode):
351 assert(self._recognized_file_type())
352 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
354 elif stat.S_ISDIR(self.mode):
355 assert(self._recognized_file_type())
356 os.mkdir(path, 0o700)
357 elif stat.S_ISCHR(self.mode):
358 assert(self._recognized_file_type())
359 os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
360 elif stat.S_ISBLK(self.mode):
361 assert(self._recognized_file_type())
362 os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
363 elif stat.S_ISFIFO(self.mode):
364 assert(self._recognized_file_type())
365 os.mkfifo(path, 0o600 | stat.S_IFIFO)
366 elif stat.S_ISSOCK(self.mode):
368 os.mknod(path, 0o600 | stat.S_IFSOCK)
370 if e.errno in (errno.EINVAL, errno.EPERM):
371 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
375 elif stat.S_ISLNK(self.mode):
376 assert(self._recognized_file_type())
377 if self.symlink_target and create_symlinks:
378 # on MacOS, symlink() permissions depend on umask, and there's
379 # no way to chown a symlink after creating it, so we have to
381 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
383 os.symlink(self.symlink_target, path)
386 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
388 assert(not self._recognized_file_type())
389 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
392 def _apply_common_rec(self, path, restore_numeric_ids=False):
394 raise ApplyError('no metadata - cannot apply to ' + path)
396 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
397 # EACCES errors at this stage are fatal for the current path.
398 if lutime and stat.S_ISLNK(self.mode):
400 lutime(path, (self.atime, self.mtime))
402 if e.errno == errno.EACCES:
403 raise ApplyError('lutime: %s' % e)
408 utime(path, (self.atime, self.mtime))
410 if e.errno == errno.EACCES:
411 raise ApplyError('utime: %s' % e)
415 uid = gid = -1 # By default, do nothing.
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'
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)
481 add_error('readlink: %s' % e)
483 def _encode_symlink_target(self):
484 return self.symlink_target
486 def _load_symlink_target_rec(self, port):
487 target = vint.read_bvec(port)
488 self.symlink_target = target
489 if self.size is None:
490 self.size = len(target)
492 assert(self.size == len(target))
497 def _add_hardlink_target(self, target):
498 self.hardlink_target = target
500 def _same_hardlink_target(self, other):
501 """Return true or false to indicate similarity in the hardlink sense."""
502 return self.hardlink_target == other.hardlink_target
504 def _encode_hardlink_target(self):
505 return self.hardlink_target
507 def _load_hardlink_target_rec(self, port):
508 self.hardlink_target = vint.read_bvec(port)
511 ## POSIX1e ACL records
513 # Recorded as a list:
514 # [txt_id_acl, num_id_acl]
515 # or, if a directory:
516 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
517 # The numeric/text distinction only matters when reading/restoring
519 def _add_posix1e_acl(self, path, st):
520 if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
522 if not stat.S_ISLNK(st.st_mode):
526 if posix1e.has_extended(path):
527 acl = posix1e.ACL(file=path)
528 acls = [acl, acl] # txt and num are the same
529 if stat.S_ISDIR(st.st_mode):
530 def_acl = posix1e.ACL(filedef=path)
531 def_acls = [def_acl, def_acl]
532 except EnvironmentError as e:
533 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
536 txt_flags = posix1e.TEXT_ABBREVIATE
537 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
538 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
539 acls[1].to_any_text('', '\n', num_flags)]
541 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
542 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
543 self.posix1e_acl = acl_rep
545 def _same_posix1e_acl(self, other):
546 """Return true or false to indicate similarity in the hardlink sense."""
547 return self.posix1e_acl == other.posix1e_acl
549 def _encode_posix1e_acl(self):
550 # Encode as two strings (w/default ACL string possibly empty).
552 acls = self.posix1e_acl
554 acls.extend(['', ''])
555 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
559 def _load_posix1e_acl_rec(self, port):
560 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
562 acl_rep = acl_rep[:2]
563 self.posix1e_acl = acl_rep
565 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
566 def apply_acl(acl_rep, kind):
568 acl = posix1e.ACL(text = acl_rep)
571 # pylibacl appears to return an IOError with errno
572 # set to 0 if a group referred to by the ACL rep
573 # doesn't exist on the current system.
574 raise ApplyError("POSIX1e ACL: can't create %r for %r"
579 acl.applyto(path, kind)
581 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
582 raise ApplyError('POSIX1e ACL applyto: %s' % e)
588 add_error("%s: can't restore ACLs; posix1e support missing.\n"
592 acls = self.posix1e_acl
594 if restore_numeric_ids:
595 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
597 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
598 if restore_numeric_ids:
599 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
601 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
604 ## Linux attributes (lsattr(1), chattr(1))
606 def _add_linux_attr(self, path, st):
607 check_linux_file_attr_api()
608 if not get_linux_file_attr: return
609 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
611 attr = get_linux_file_attr(path)
613 self.linux_attr = attr
615 if e.errno == errno.EACCES:
616 add_error('read Linux attr: %s' % e)
617 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
618 # Assume filesystem doesn't support attrs.
620 elif e.errno == EINVAL:
621 global _warned_about_attr_einval
622 if not _warned_about_attr_einval:
623 log("Ignoring attr EINVAL;"
624 + " if you're not using ntfs-3g, please report: "
626 _warned_about_attr_einval = True
631 def _same_linux_attr(self, other):
632 """Return true or false to indicate similarity in the hardlink sense."""
633 return self.linux_attr == other.linux_attr
635 def _encode_linux_attr(self):
637 return vint.pack('V', self.linux_attr)
641 def _load_linux_attr_rec(self, port):
642 data = vint.read_bvec(port)
643 self.linux_attr = vint.unpack('V', data)[0]
645 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
647 check_linux_file_attr_api()
648 if not set_linux_file_attr:
649 add_error("%s: can't restore linuxattrs: "
650 "linuxattr support missing.\n" % path)
653 set_linux_file_attr(path, self.linux_attr)
655 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
656 raise ApplyError('Linux chattr: %s (0x%s)'
657 % (e, hex(self.linux_attr)))
658 elif e.errno == EINVAL:
659 msg = "if you're not using ntfs-3g, please report"
660 raise ApplyError('Linux chattr: %s (0x%s) (%s)'
661 % (e, hex(self.linux_attr), msg))
666 ## Linux extended attributes (getfattr(1), setfattr(1))
668 def _add_linux_xattr(self, path, st):
671 self.linux_xattr = xattr.get_all(path, nofollow=True)
672 except EnvironmentError as e:
673 if e.errno != errno.EOPNOTSUPP:
676 def _same_linux_xattr(self, other):
677 """Return true or false to indicate similarity in the hardlink sense."""
678 return self.linux_xattr == other.linux_xattr
680 def _encode_linux_xattr(self):
682 result = vint.pack('V', len(self.linux_xattr))
683 for name, value in self.linux_xattr:
684 result += vint.pack('ss', name, value)
689 def _load_linux_xattr_rec(self, file):
690 data = vint.read_bvec(file)
691 memfile = BytesIO(data)
693 for i in range(vint.read_vuint(memfile)):
694 key = vint.read_bvec(memfile)
695 value = vint.read_bvec(memfile)
696 result.append((key, value))
697 self.linux_xattr = result
699 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
702 add_error("%s: can't restore xattr; xattr support missing.\n"
705 if not self.linux_xattr:
708 existing_xattrs = set(xattr.list(path, nofollow=True))
710 if e.errno == errno.EACCES:
711 raise ApplyError('xattr.set %r: %s' % (path, e))
714 for k, v in self.linux_xattr:
715 if k not in existing_xattrs \
716 or v != xattr.get(path, k, nofollow=True):
718 xattr.set(path, k, v, nofollow=True)
720 if e.errno == errno.EPERM \
721 or e.errno == errno.EOPNOTSUPP:
722 raise ApplyError('xattr.set %r: %s' % (path, e))
725 existing_xattrs -= frozenset([k])
726 for k in existing_xattrs:
728 xattr.remove(path, k, nofollow=True)
730 if e.errno in (errno.EPERM, errno.EACCES):
731 raise ApplyError('xattr.remove %r: %s' % (path, e))
736 self.mode = self.uid = self.gid = self.user = self.group = None
737 self.atime = self.mtime = self.ctime = None
741 self.symlink_target = None
742 self.hardlink_target = None
743 self.linux_attr = None
744 self.linux_xattr = None
745 self.posix1e_acl = None
747 def __eq__(self, other):
748 if not isinstance(other, Metadata): return False
749 if self.mode != other.mode: return False
750 if self.mtime != other.mtime: return False
751 if self.ctime != other.ctime: return False
752 if self.atime != other.atime: return False
753 if self.path != other.path: return False
754 if self.uid != other.uid: return False
755 if self.gid != other.gid: return False
756 if self.size != other.size: return False
757 if self.user != other.user: return False
758 if self.group != other.group: return False
759 if self.symlink_target != other.symlink_target: return False
760 if self.hardlink_target != other.hardlink_target: return False
761 if self.linux_attr != other.linux_attr: return False
762 if self.posix1e_acl != other.posix1e_acl: return False
765 def __ne__(self, other):
766 return not self.__eq__(other)
769 return hash((self.mode,
780 self.hardlink_target,
785 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
786 if self.path is not None:
787 result += ' path:' + repr(self.path)
788 if self.mode is not None:
789 result += ' mode:' + repr(xstat.mode_str(self.mode)
790 + '(%s)' % oct(self.mode))
791 if self.uid is not None:
792 result += ' uid:' + str(self.uid)
793 if self.gid is not None:
794 result += ' gid:' + str(self.gid)
795 if self.user is not None:
796 result += ' user:' + repr(self.user)
797 if self.group is not None:
798 result += ' group:' + repr(self.group)
799 if self.size is not None:
800 result += ' size:' + repr(self.size)
801 for name, val in (('atime', self.atime),
802 ('mtime', self.mtime),
803 ('ctime', self.ctime)):
805 result += ' %s:%r (%d)' \
807 strftime('%Y-%m-%d %H:%M %z',
808 gmtime(xstat.fstime_floor_secs(val))),
811 return ''.join(result)
813 def write(self, port, include_path=True):
814 records = include_path and [(_rec_tag_path, self._encode_path())] or []
815 records.extend([(_rec_tag_common_v3, self._encode_common()),
816 (_rec_tag_symlink_target,
817 self._encode_symlink_target()),
818 (_rec_tag_hardlink_target,
819 self._encode_hardlink_target()),
820 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
821 (_rec_tag_linux_attr, self._encode_linux_attr()),
822 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
823 for tag, data in records:
825 vint.write_vuint(port, tag)
826 vint.write_bvec(port, data)
827 vint.write_vuint(port, _rec_tag_end)
829 def encode(self, include_path=True):
831 self.write(port, include_path)
832 return port.getvalue()
835 return deepcopy(self)
839 # This method should either return a valid Metadata object,
840 # return None if there was no information at all (just a
841 # _rec_tag_end), throw EOFError if there was nothing at all to
842 # read, or throw an Exception if a valid object could not be
844 tag = vint.read_vuint(port)
845 if tag == _rec_tag_end:
847 try: # From here on, EOF is an error.
849 while True: # only exit is error (exception) or _rec_tag_end
850 if tag == _rec_tag_path:
851 result._load_path_rec(port)
852 elif tag == _rec_tag_common_v3:
853 result._load_common_rec(port, version=3)
854 elif tag == _rec_tag_common_v2:
855 result._load_common_rec(port, version=2)
856 elif tag == _rec_tag_symlink_target:
857 result._load_symlink_target_rec(port)
858 elif tag == _rec_tag_hardlink_target:
859 result._load_hardlink_target_rec(port)
860 elif tag == _rec_tag_posix1e_acl:
861 result._load_posix1e_acl_rec(port)
862 elif tag == _rec_tag_linux_attr:
863 result._load_linux_attr_rec(port)
864 elif tag == _rec_tag_linux_xattr:
865 result._load_linux_xattr_rec(port)
866 elif tag == _rec_tag_end:
868 elif tag == _rec_tag_common_v1: # Should be very rare.
869 result._load_common_rec(port, version=1)
870 else: # unknown record
872 tag = vint.read_vuint(port)
874 raise Exception("EOF while reading Metadata")
877 return stat.S_ISDIR(self.mode)
879 def create_path(self, path, create_symlinks=True):
880 self._create_via_common_rec(path, create_symlinks=create_symlinks)
882 def apply_to_path(self, path=None, restore_numeric_ids=False):
883 # apply metadata to path -- file must exist
887 raise Exception('Metadata.apply_to_path() called with no path')
888 if not self._recognized_file_type():
889 add_error('not applying metadata to "%s"' % path
890 + ' with unrecognized mode "0x%x"\n' % self.mode)
892 num_ids = restore_numeric_ids
893 for apply_metadata in (self._apply_common_rec,
894 self._apply_posix1e_acl_rec,
895 self._apply_linux_attr_rec,
896 self._apply_linux_xattr_rec):
898 apply_metadata(path, restore_numeric_ids=num_ids)
899 except ApplyError as e:
902 def same_file(self, other):
903 """Compare this to other for equivalency. Return true if
904 their information implies they could represent the same file
905 on disk, in the hardlink sense. Assume they're both regular
907 return self._same_common(other) \
908 and self._same_hardlink_target(other) \
909 and self._same_posix1e_acl(other) \
910 and self._same_linux_attr(other) \
911 and self._same_linux_xattr(other)
914 def from_path(path, statinfo=None, archive_path=None,
915 save_symlinks=True, hardlink_target=None,
917 """Return the metadata associated with the path. When normalized is
918 true, return the metadata appropriate for a typical save, which
919 may or may not be all of it."""
921 result.path = archive_path
922 st = statinfo or xstat.lstat(path)
923 result._add_common(path, st)
925 result._add_symlink_target(path, st)
926 result._add_hardlink_target(hardlink_target)
927 result._add_posix1e_acl(path, st)
928 result._add_linux_attr(path, st)
929 result._add_linux_xattr(path, st)
931 # Only store sizes for regular files and symlinks for now.
932 if not (stat.S_ISREG(result.mode) or stat.S_ISLNK(result.mode)):
937 def save_tree(output_file, paths,
943 # Issue top-level rewrite warnings.
945 safe_path = _clean_up_path_for_archive(path)
946 if safe_path != path:
947 log('archiving "%s" as "%s"\n' % (path, safe_path))
951 safe_path = _clean_up_path_for_archive(p)
953 if stat.S_ISDIR(st.st_mode):
955 m = from_path(p, statinfo=st, archive_path=safe_path,
956 save_symlinks=save_symlinks)
958 print(m.path, file=sys.stderr)
959 m.write(output_file, include_path=write_paths)
961 start_dir = os.getcwd()
963 for (p, st) in recursive_dirlist(paths, xdev=xdev):
964 dirlist_dir = os.getcwd()
966 safe_path = _clean_up_path_for_archive(p)
967 m = from_path(p, statinfo=st, archive_path=safe_path,
968 save_symlinks=save_symlinks)
970 print(m.path, file=sys.stderr)
971 m.write(output_file, include_path=write_paths)
972 os.chdir(dirlist_dir)
977 def _set_up_path(meta, create_symlinks=True):
978 # Allow directories to exist as a special case -- might have
979 # been created by an earlier longer path.
983 parent = os.path.dirname(meta.path)
986 meta.create_path(meta.path, create_symlinks=create_symlinks)
989 all_fields = frozenset(['path',
1006 def summary_str(meta, numeric_ids = False, classification = None,
1007 human_readable = False):
1009 """Return a string containing the "ls -l" style listing for meta.
1010 Classification may be "all", "type", or None."""
1011 user_str = group_str = size_or_dev_str = '?'
1012 symlink_target = None
1015 mode_str = xstat.mode_str(meta.mode)
1016 symlink_target = meta.symlink_target
1017 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
1018 mtime_str = strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
1019 if meta.user and not numeric_ids:
1020 user_str = meta.user
1021 elif meta.uid != None:
1022 user_str = str(meta.uid)
1023 if meta.group and not numeric_ids:
1024 group_str = meta.group
1025 elif meta.gid != None:
1026 group_str = str(meta.gid)
1027 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
1029 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
1030 os.minor(meta.rdev))
1031 elif meta.size != None:
1033 size_or_dev_str = format_filesize(meta.size)
1035 size_or_dev_str = str(meta.size)
1037 size_or_dev_str = '-'
1039 classification_str = \
1040 xstat.classification_str(meta.mode, classification == 'all')
1043 mtime_str = '????-??-?? ??:??'
1044 classification_str = '?'
1048 name += classification_str
1050 name += ' -> ' + meta.symlink_target
1052 return '%-10s %-11s %11s %16s %s' % (mode_str,
1053 user_str + "/" + group_str,
1059 def detailed_str(meta, fields = None):
1060 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
1061 # 0", "link-target:", etc.
1066 if 'path' in fields:
1067 path = meta.path or ''
1068 result.append('path: ' + path)
1069 if 'mode' in fields:
1070 result.append('mode: %s (%s)' % (oct(meta.mode),
1071 xstat.mode_str(meta.mode)))
1072 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1073 result.append('link-target: ' + meta.symlink_target)
1074 if 'rdev' in fields:
1076 result.append('rdev: %d,%d' % (os.major(meta.rdev),
1077 os.minor(meta.rdev)))
1079 result.append('rdev: 0')
1080 if 'size' in fields and meta.size is not None:
1081 result.append('size: ' + str(meta.size))
1083 result.append('uid: ' + str(meta.uid))
1085 result.append('gid: ' + str(meta.gid))
1086 if 'user' in fields:
1087 result.append('user: ' + meta.user)
1088 if 'group' in fields:
1089 result.append('group: ' + meta.group)
1090 if 'atime' in fields:
1091 # If we don't have xstat.lutime, that means we have to use
1092 # utime(), and utime() has no way to set the mtime/atime of a
1093 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1094 # so let's not report it. (That way scripts comparing
1095 # before/after won't trigger.)
1096 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1097 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1099 result.append('atime: 0')
1100 if 'mtime' in fields:
1101 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1102 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1104 result.append('mtime: 0')
1105 if 'ctime' in fields:
1106 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1107 if 'linux-attr' in fields and meta.linux_attr:
1108 result.append('linux-attr: ' + hex(meta.linux_attr))
1109 if 'linux-xattr' in fields and meta.linux_xattr:
1110 for name, value in meta.linux_xattr:
1111 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1112 if 'posix1e-acl' in fields and meta.posix1e_acl:
1113 acl = meta.posix1e_acl[0]
1114 result.append('posix1e-acl: ' + acl + '\n')
1115 if stat.S_ISDIR(meta.mode):
1116 def_acl = meta.posix1e_acl[2]
1117 result.append('posix1e-acl-default: ' + def_acl + '\n')
1118 return '\n'.join(result)
1121 class _ArchiveIterator:
1124 return Metadata.read(self._file)
1126 raise StopIteration()
1131 def __init__(self, file):
1135 def display_archive(file):
1138 for meta in _ArchiveIterator(file):
1141 print(detailed_str(meta))
1144 for meta in _ArchiveIterator(file):
1145 print(summary_str(meta))
1147 for meta in _ArchiveIterator(file):
1149 print('bup: no metadata path, but asked to only display path'
1150 '(increase verbosity?)')
1155 def start_extract(file, create_symlinks=True):
1156 for meta in _ArchiveIterator(file):
1157 if not meta: # Hit end record.
1160 print(meta.path, file=sys.stderr)
1161 xpath = _clean_up_extract_path(meta.path)
1163 add_error(Exception('skipping risky path "%s"' % meta.path))
1166 _set_up_path(meta, create_symlinks=create_symlinks)
1169 def finish_extract(file, restore_numeric_ids=False):
1171 for meta in _ArchiveIterator(file):
1172 if not meta: # Hit end record.
1174 xpath = _clean_up_extract_path(meta.path)
1176 add_error(Exception('skipping risky path "%s"' % dir.path))
1178 if os.path.isdir(meta.path):
1179 all_dirs.append(meta)
1182 print(meta.path, file=sys.stderr)
1183 meta.apply_to_path(path=xpath,
1184 restore_numeric_ids=restore_numeric_ids)
1185 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1186 for dir in all_dirs:
1187 # Don't need to check xpath -- won't be in all_dirs if not OK.
1188 xpath = _clean_up_extract_path(dir.path)
1190 print(dir.path, file=sys.stderr)
1191 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1194 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1195 # For now, just store all the directories and handle them last,
1198 for meta in _ArchiveIterator(file):
1199 if not meta: # Hit end record.
1201 xpath = _clean_up_extract_path(meta.path)
1203 add_error(Exception('skipping risky path "%s"' % meta.path))
1207 print('+', meta.path, file=sys.stderr)
1208 _set_up_path(meta, create_symlinks=create_symlinks)
1209 if os.path.isdir(meta.path):
1210 all_dirs.append(meta)
1213 print('=', meta.path, file=sys.stderr)
1214 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1215 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1216 for dir in all_dirs:
1217 # Don't need to check xpath -- won't be in all_dirs if not OK.
1218 xpath = _clean_up_extract_path(dir.path)
1220 print('=', xpath, file=sys.stderr)
1221 # Shouldn't have to check for risky paths here (omitted above).
1222 dir.apply_to_path(path=dir.path,
1223 restore_numeric_ids=restore_numeric_ids)