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 errno import EACCES, EINVAL, ENOTTY, ENOSYS, EOPNOTSUPP
10 import errno, os, sys, stat, time, pwd, grp, socket, struct
12 from bup import vint, xstat
13 from bup.drecurse import recursive_dirlist
14 from bup.helpers import add_error, mkdirp, log, is_superuser, format_filesize
15 from bup.helpers import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
16 from bup.xstat import utime, lutime
19 if sys.platform.startswith('linux'):
23 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
27 except AttributeError:
28 log('Warning: python-xattr module is too old; '
29 'install python-pyxattr instead.\n')
33 if not (sys.platform.startswith('cygwin') \
34 or sys.platform.startswith('darwin') \
35 or sys.platform.startswith('netbsd')):
39 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
42 from bup._helpers import get_linux_file_attr, set_linux_file_attr
44 # No need for a warning here; the only reason they won't exist is that we're
45 # not on Linux, in which case files don't have any linux attrs anyway, so
46 # lacking the functions isn't a problem.
47 get_linux_file_attr = set_linux_file_attr = None
50 # See the bup_get_linux_file_attr() comments.
51 _suppress_linux_file_attr = \
52 sys.byteorder == 'big' and struct.calcsize('@l') > struct.calcsize('@i')
54 def check_linux_file_attr_api():
55 global get_linux_file_attr, set_linux_file_attr
56 if not (get_linux_file_attr or set_linux_file_attr):
58 if _suppress_linux_file_attr:
59 log('Warning: Linux attr support disabled (see "bup help index").\n')
60 get_linux_file_attr = set_linux_file_attr = None
63 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
65 # Q: Consider hardlink support?
66 # Q: Is it OK to store raw linux attr (chattr) flags?
67 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
68 # Q: Is the application of posix1e has_extended() correct?
69 # Q: Is one global --numeric-ids argument sufficient?
70 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
71 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
73 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
74 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
75 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
76 # FIXME: Consider pack('vvvvsss', ...) optimization.
80 # osx (varies between hfs and hfs+):
81 # type - regular dir char block fifo socket ...
82 # perms - rwxrwxrwxsgt
83 # times - ctime atime mtime
86 # hard-link-info (hfs+ only)
89 # attributes-osx see chflags
95 # type - regular dir ...
96 # times - creation, modification, posix change, access
99 # attributes - see attrib
101 # forks (alternate data streams)
105 # type - regular dir ...
106 # perms - rwxrwxrwx (maybe - see wikipedia)
107 # times - creation, modification, access
108 # attributes - see attrib
112 _have_lchmod = hasattr(os, 'lchmod')
115 def _clean_up_path_for_archive(p):
116 # Not the most efficient approach.
119 # Take everything after any '/../'.
120 pos = result.rfind('/../')
122 result = result[result.rfind('/../') + 4:]
124 # Take everything after any remaining '../'.
125 if result.startswith("../"):
128 # Remove any '/./' sequences.
129 pos = result.find('/./')
131 result = result[0:pos] + '/' + result[pos + 3:]
132 pos = result.find('/./')
134 # Remove any leading '/'s.
135 result = result.lstrip('/')
137 # Replace '//' with '/' everywhere.
138 pos = result.find('//')
140 result = result[0:pos] + '/' + result[pos + 2:]
141 pos = result.find('//')
143 # Take everything after any remaining './'.
144 if result.startswith('./'):
147 # Take everything before any remaining '/.'.
148 if result.endswith('/.'):
151 if result == '' or result.endswith('/..'):
158 if p.startswith('/'):
160 if p.find('/../') != -1:
162 if p.startswith('../'):
164 if p.endswith('/..'):
169 def _clean_up_extract_path(p):
170 result = p.lstrip('/')
173 elif _risky_path(result):
179 # These tags are currently conceptually private to Metadata, and they
180 # must be unique, and must *never* be changed.
183 _rec_tag_common = 2 # times, user, group, type, perms, etc. (legacy/broken)
184 _rec_tag_symlink_target = 3
185 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
186 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
187 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
188 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
189 _rec_tag_hardlink_target = 8 # hard link target path
190 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
192 _warned_about_attr_einval = None
195 class ApplyError(Exception):
196 # Thrown when unable to apply any given bit of metadata to a path.
201 # Metadata is stored as a sequence of tagged binary records. Each
202 # record will have some subset of add, encode, load, create, and
203 # apply methods, i.e. _add_foo...
205 # We do allow an "empty" object as a special case, i.e. no
206 # records. One can be created by trying to write Metadata(), and
207 # for such an object, read() will return None. This is used by
208 # "bup save", for example, as a placeholder in cases where
211 # NOTE: if any relevant fields are added or removed, be sure to
212 # update same_file() below.
216 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
217 # must be non-negative and < 10**9.
219 def _add_common(self, path, st):
220 assert(st.st_uid >= 0)
221 assert(st.st_gid >= 0)
224 self.atime = st.st_atime
225 self.mtime = st.st_mtime
226 self.ctime = st.st_ctime
227 self.user = self.group = ''
228 entry = pwd_from_uid(st.st_uid)
230 self.user = entry.pw_name
231 entry = grp_from_gid(st.st_gid)
233 self.group = entry.gr_name
234 self.mode = st.st_mode
235 # Only collect st_rdev if we might need it for a mknod()
236 # during restore. On some platforms (i.e. kFreeBSD), it isn't
237 # stable for other file types. For example "cp -a" will
238 # change it for a plain file.
239 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
240 self.rdev = st.st_rdev
244 def _same_common(self, other):
245 """Return true or false to indicate similarity in the hardlink sense."""
246 return self.uid == other.uid \
247 and self.gid == other.gid \
248 and self.rdev == other.rdev \
249 and self.mtime == other.mtime \
250 and self.ctime == other.ctime \
251 and self.user == other.user \
252 and self.group == other.group
254 def _encode_common(self):
257 atime = xstat.nsecs_to_timespec(self.atime)
258 mtime = xstat.nsecs_to_timespec(self.mtime)
259 ctime = xstat.nsecs_to_timespec(self.ctime)
260 result = vint.pack('vvsvsvvVvVvV',
275 def _load_common_rec(self, port, legacy_format=False):
276 unpack_fmt = 'vvsvsvvVvVvV'
278 unpack_fmt = 'VVsVsVvVvVvV'
279 data = vint.read_bvec(port)
291 ctime_ns) = vint.unpack(unpack_fmt, data)
292 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
293 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
294 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
296 def _recognized_file_type(self):
297 return stat.S_ISREG(self.mode) \
298 or stat.S_ISDIR(self.mode) \
299 or stat.S_ISCHR(self.mode) \
300 or stat.S_ISBLK(self.mode) \
301 or stat.S_ISFIFO(self.mode) \
302 or stat.S_ISSOCK(self.mode) \
303 or stat.S_ISLNK(self.mode)
305 def _create_via_common_rec(self, path, create_symlinks=True):
307 raise ApplyError('no metadata - cannot create path ' + path)
309 # If the path already exists and is a dir, try rmdir.
310 # If the path already exists and is anything else, try unlink.
313 st = xstat.lstat(path)
315 if e.errno != errno.ENOENT:
318 if stat.S_ISDIR(st.st_mode):
322 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
323 msg = 'refusing to overwrite non-empty dir ' + path
329 if stat.S_ISREG(self.mode):
330 assert(self._recognized_file_type())
331 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
333 elif stat.S_ISDIR(self.mode):
334 assert(self._recognized_file_type())
335 os.mkdir(path, 0o700)
336 elif stat.S_ISCHR(self.mode):
337 assert(self._recognized_file_type())
338 os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
339 elif stat.S_ISBLK(self.mode):
340 assert(self._recognized_file_type())
341 os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
342 elif stat.S_ISFIFO(self.mode):
343 assert(self._recognized_file_type())
344 os.mknod(path, 0o600 | stat.S_IFIFO)
345 elif stat.S_ISSOCK(self.mode):
347 os.mknod(path, 0o600 | stat.S_IFSOCK)
349 if e.errno in (errno.EINVAL, errno.EPERM):
350 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
354 elif stat.S_ISLNK(self.mode):
355 assert(self._recognized_file_type())
356 if self.symlink_target and create_symlinks:
357 # on MacOS, symlink() permissions depend on umask, and there's
358 # no way to chown a symlink after creating it, so we have to
360 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
362 os.symlink(self.symlink_target, path)
365 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
367 assert(not self._recognized_file_type())
368 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
371 def _apply_common_rec(self, path, restore_numeric_ids=False):
373 raise ApplyError('no metadata - cannot apply to ' + path)
375 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
376 # EACCES errors at this stage are fatal for the current path.
377 if lutime and stat.S_ISLNK(self.mode):
379 lutime(path, (self.atime, self.mtime))
381 if e.errno == errno.EACCES:
382 raise ApplyError('lutime: %s' % e)
387 utime(path, (self.atime, self.mtime))
389 if e.errno == errno.EACCES:
390 raise ApplyError('utime: %s' % e)
394 uid = gid = -1 # By default, do nothing.
398 if not restore_numeric_ids:
399 if self.uid != 0 and self.user:
400 entry = pwd_from_name(self.user)
403 if self.gid != 0 and self.group:
404 entry = grp_from_name(self.group)
407 else: # not superuser - only consider changing the group/gid
408 user_gids = os.getgroups()
409 if self.gid in user_gids:
411 if not restore_numeric_ids and self.gid != 0:
412 # The grp might not exist on the local system.
413 grps = filter(None, [grp_from_gid(x) for x in user_gids])
414 if self.group in [x.gr_name for x in grps]:
415 g = grp_from_name(self.group)
419 if uid != -1 or gid != -1:
421 os.lchown(path, uid, gid)
423 if e.errno == errno.EPERM:
424 add_error('lchown: %s' % e)
425 elif sys.platform.startswith('cygwin') \
426 and e.errno == errno.EINVAL:
427 add_error('lchown: unknown uid/gid (%d/%d) for %s'
434 os.lchmod(path, stat.S_IMODE(self.mode))
435 except errno.ENOSYS: # Function not implemented
437 elif not stat.S_ISLNK(self.mode):
438 os.chmod(path, stat.S_IMODE(self.mode))
443 def _encode_path(self):
445 return vint.pack('s', self.path)
449 def _load_path_rec(self, port):
450 self.path = vint.unpack('s', vint.read_bvec(port))[0]
455 def _add_symlink_target(self, path, st):
457 if stat.S_ISLNK(st.st_mode):
458 self.symlink_target = os.readlink(path)
460 add_error('readlink: %s' % e)
462 def _encode_symlink_target(self):
463 return self.symlink_target
465 def _load_symlink_target_rec(self, port):
466 target = vint.read_bvec(port)
467 self.symlink_target = target
468 self.size = len(target)
473 def _add_hardlink_target(self, target):
474 self.hardlink_target = target
476 def _same_hardlink_target(self, other):
477 """Return true or false to indicate similarity in the hardlink sense."""
478 return self.hardlink_target == other.hardlink_target
480 def _encode_hardlink_target(self):
481 return self.hardlink_target
483 def _load_hardlink_target_rec(self, port):
484 self.hardlink_target = vint.read_bvec(port)
487 ## POSIX1e ACL records
489 # Recorded as a list:
490 # [txt_id_acl, num_id_acl]
491 # or, if a directory:
492 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
493 # The numeric/text distinction only matters when reading/restoring
495 def _add_posix1e_acl(self, path, st):
496 if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
498 if not stat.S_ISLNK(st.st_mode):
502 if posix1e.has_extended(path):
503 acl = posix1e.ACL(file=path)
504 acls = [acl, acl] # txt and num are the same
505 if stat.S_ISDIR(st.st_mode):
506 def_acl = posix1e.ACL(filedef=path)
507 def_acls = [def_acl, def_acl]
508 except EnvironmentError as e:
509 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
512 txt_flags = posix1e.TEXT_ABBREVIATE
513 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
514 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
515 acls[1].to_any_text('', '\n', num_flags)]
517 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
518 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
519 self.posix1e_acl = acl_rep
521 def _same_posix1e_acl(self, other):
522 """Return true or false to indicate similarity in the hardlink sense."""
523 return self.posix1e_acl == other.posix1e_acl
525 def _encode_posix1e_acl(self):
526 # Encode as two strings (w/default ACL string possibly empty).
528 acls = self.posix1e_acl
530 acls.extend(['', ''])
531 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
535 def _load_posix1e_acl_rec(self, port):
536 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
538 acl_rep = acl_rep[:2]
539 self.posix1e_acl = acl_rep
541 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
542 def apply_acl(acl_rep, kind):
544 acl = posix1e.ACL(text = acl_rep)
547 # pylibacl appears to return an IOError with errno
548 # set to 0 if a group referred to by the ACL rep
549 # doesn't exist on the current system.
550 raise ApplyError("POSIX1e ACL: can't create %r for %r"
555 acl.applyto(path, kind)
557 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
558 raise ApplyError('POSIX1e ACL applyto: %s' % e)
564 add_error("%s: can't restore ACLs; posix1e support missing.\n"
568 acls = self.posix1e_acl
570 if restore_numeric_ids:
571 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
573 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
574 if restore_numeric_ids:
575 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
577 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
580 ## Linux attributes (lsattr(1), chattr(1))
582 def _add_linux_attr(self, path, st):
583 check_linux_file_attr_api()
584 if not get_linux_file_attr: return
585 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
587 attr = get_linux_file_attr(path)
589 self.linux_attr = attr
591 if e.errno == errno.EACCES:
592 add_error('read Linux attr: %s' % e)
593 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
594 # Assume filesystem doesn't support attrs.
596 elif e.errno == EINVAL:
597 global _warned_about_attr_einval
598 if not _warned_about_attr_einval:
599 log("Ignoring attr EINVAL;"
600 + " if you're not using ntfs-3g, please report: "
602 _warned_about_attr_einval = True
607 def _same_linux_attr(self, other):
608 """Return true or false to indicate similarity in the hardlink sense."""
609 return self.linux_attr == other.linux_attr
611 def _encode_linux_attr(self):
613 return vint.pack('V', self.linux_attr)
617 def _load_linux_attr_rec(self, port):
618 data = vint.read_bvec(port)
619 self.linux_attr = vint.unpack('V', data)[0]
621 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
623 check_linux_file_attr_api()
624 if not set_linux_file_attr:
625 add_error("%s: can't restore linuxattrs: "
626 "linuxattr support missing.\n" % path)
629 set_linux_file_attr(path, self.linux_attr)
631 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
632 raise ApplyError('Linux chattr: %s (0x%s)'
633 % (e, hex(self.linux_attr)))
634 elif e.errno == EINVAL:
635 msg = "if you're not using ntfs-3g, please report"
636 raise ApplyError('Linux chattr: %s (0x%s) (%s)'
637 % (e, hex(self.linux_attr), msg))
642 ## Linux extended attributes (getfattr(1), setfattr(1))
644 def _add_linux_xattr(self, path, st):
647 self.linux_xattr = xattr.get_all(path, nofollow=True)
648 except EnvironmentError as e:
649 if e.errno != errno.EOPNOTSUPP:
652 def _same_linux_xattr(self, other):
653 """Return true or false to indicate similarity in the hardlink sense."""
654 return self.linux_xattr == other.linux_xattr
656 def _encode_linux_xattr(self):
658 result = vint.pack('V', len(self.linux_xattr))
659 for name, value in self.linux_xattr:
660 result += vint.pack('ss', name, value)
665 def _load_linux_xattr_rec(self, file):
666 data = vint.read_bvec(file)
667 memfile = BytesIO(data)
669 for i in range(vint.read_vuint(memfile)):
670 key = vint.read_bvec(memfile)
671 value = vint.read_bvec(memfile)
672 result.append((key, value))
673 self.linux_xattr = result
675 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
678 add_error("%s: can't restore xattr; xattr support missing.\n"
681 if not self.linux_xattr:
684 existing_xattrs = set(xattr.list(path, nofollow=True))
686 if e.errno == errno.EACCES:
687 raise ApplyError('xattr.set %r: %s' % (path, e))
690 for k, v in self.linux_xattr:
691 if k not in existing_xattrs \
692 or v != xattr.get(path, k, nofollow=True):
694 xattr.set(path, k, v, nofollow=True)
696 if e.errno == errno.EPERM \
697 or e.errno == errno.EOPNOTSUPP:
698 raise ApplyError('xattr.set %r: %s' % (path, e))
701 existing_xattrs -= frozenset([k])
702 for k in existing_xattrs:
704 xattr.remove(path, k, nofollow=True)
706 if e.errno in (errno.EPERM, errno.EACCES):
707 raise ApplyError('xattr.remove %r: %s' % (path, e))
712 self.mode = self.uid = self.gid = self.user = self.group = None
713 self.atime = self.mtime = self.ctime = None
717 self.symlink_target = None
718 self.hardlink_target = None
719 self.linux_attr = None
720 self.linux_xattr = None
721 self.posix1e_acl = None
724 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
726 result += ' path:' + repr(self.path)
728 result += ' mode:' + repr(xstat.mode_str(self.mode)
729 + '(%s)' % hex(self.mode))
731 result += ' uid:' + str(self.uid)
733 result += ' gid:' + str(self.gid)
735 result += ' user:' + repr(self.user)
737 result += ' group:' + repr(self.group)
739 result += ' size:' + repr(self.size)
740 for name, val in (('atime', self.atime),
741 ('mtime', self.mtime),
742 ('ctime', self.ctime)):
745 time.strftime('%Y-%m-%d %H:%M %z',
746 time.gmtime(xstat.fstime_floor_secs(val))))
748 return ''.join(result)
750 def write(self, port, include_path=True):
751 records = include_path and [(_rec_tag_path, self._encode_path())] or []
752 records.extend([(_rec_tag_common_v2, self._encode_common()),
753 (_rec_tag_symlink_target,
754 self._encode_symlink_target()),
755 (_rec_tag_hardlink_target,
756 self._encode_hardlink_target()),
757 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
758 (_rec_tag_linux_attr, self._encode_linux_attr()),
759 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
760 for tag, data in records:
762 vint.write_vuint(port, tag)
763 vint.write_bvec(port, data)
764 vint.write_vuint(port, _rec_tag_end)
766 def encode(self, include_path=True):
768 self.write(port, include_path)
769 return port.getvalue()
773 # This method should either return a valid Metadata object,
774 # return None if there was no information at all (just a
775 # _rec_tag_end), throw EOFError if there was nothing at all to
776 # read, or throw an Exception if a valid object could not be
778 tag = vint.read_vuint(port)
779 if tag == _rec_tag_end:
781 try: # From here on, EOF is an error.
783 while True: # only exit is error (exception) or _rec_tag_end
784 if tag == _rec_tag_path:
785 result._load_path_rec(port)
786 elif tag == _rec_tag_common_v2:
787 result._load_common_rec(port)
788 elif tag == _rec_tag_symlink_target:
789 result._load_symlink_target_rec(port)
790 elif tag == _rec_tag_hardlink_target:
791 result._load_hardlink_target_rec(port)
792 elif tag == _rec_tag_posix1e_acl:
793 result._load_posix1e_acl_rec(port)
794 elif tag == _rec_tag_linux_attr:
795 result._load_linux_attr_rec(port)
796 elif tag == _rec_tag_linux_xattr:
797 result._load_linux_xattr_rec(port)
798 elif tag == _rec_tag_end:
800 elif tag == _rec_tag_common: # Should be very rare.
801 result._load_common_rec(port, legacy_format = True)
802 else: # unknown record
804 tag = vint.read_vuint(port)
806 raise Exception("EOF while reading Metadata")
809 return stat.S_ISDIR(self.mode)
811 def create_path(self, path, create_symlinks=True):
812 self._create_via_common_rec(path, create_symlinks=create_symlinks)
814 def apply_to_path(self, path=None, restore_numeric_ids=False):
815 # apply metadata to path -- file must exist
819 raise Exception('Metadata.apply_to_path() called with no path')
820 if not self._recognized_file_type():
821 add_error('not applying metadata to "%s"' % path
822 + ' with unrecognized mode "0x%x"\n' % self.mode)
824 num_ids = restore_numeric_ids
825 for apply_metadata in (self._apply_common_rec,
826 self._apply_posix1e_acl_rec,
827 self._apply_linux_attr_rec,
828 self._apply_linux_xattr_rec):
830 apply_metadata(path, restore_numeric_ids=num_ids)
831 except ApplyError as e:
834 def same_file(self, other):
835 """Compare this to other for equivalency. Return true if
836 their information implies they could represent the same file
837 on disk, in the hardlink sense. Assume they're both regular
839 return self._same_common(other) \
840 and self._same_hardlink_target(other) \
841 and self._same_posix1e_acl(other) \
842 and self._same_linux_attr(other) \
843 and self._same_linux_xattr(other)
846 def from_path(path, statinfo=None, archive_path=None,
847 save_symlinks=True, hardlink_target=None):
849 result.path = archive_path
850 st = statinfo or xstat.lstat(path)
851 result.size = st.st_size
852 result._add_common(path, st)
854 result._add_symlink_target(path, st)
855 result._add_hardlink_target(hardlink_target)
856 result._add_posix1e_acl(path, st)
857 result._add_linux_attr(path, st)
858 result._add_linux_xattr(path, st)
862 def save_tree(output_file, paths,
868 # Issue top-level rewrite warnings.
870 safe_path = _clean_up_path_for_archive(path)
871 if safe_path != path:
872 log('archiving "%s" as "%s"\n' % (path, safe_path))
876 safe_path = _clean_up_path_for_archive(p)
878 if stat.S_ISDIR(st.st_mode):
880 m = from_path(p, statinfo=st, archive_path=safe_path,
881 save_symlinks=save_symlinks)
883 print >> sys.stderr, m.path
884 m.write(output_file, include_path=write_paths)
886 start_dir = os.getcwd()
888 for (p, st) in recursive_dirlist(paths, xdev=xdev):
889 dirlist_dir = os.getcwd()
891 safe_path = _clean_up_path_for_archive(p)
892 m = from_path(p, statinfo=st, archive_path=safe_path,
893 save_symlinks=save_symlinks)
895 print >> sys.stderr, m.path
896 m.write(output_file, include_path=write_paths)
897 os.chdir(dirlist_dir)
902 def _set_up_path(meta, create_symlinks=True):
903 # Allow directories to exist as a special case -- might have
904 # been created by an earlier longer path.
908 parent = os.path.dirname(meta.path)
911 meta.create_path(meta.path, create_symlinks=create_symlinks)
914 all_fields = frozenset(['path',
931 def summary_str(meta, numeric_ids = False, classification = None,
932 human_readable = False):
934 """Return a string containing the "ls -l" style listing for meta.
935 Classification may be "all", "type", or None."""
936 user_str = group_str = size_or_dev_str = '?'
937 symlink_target = None
940 mode_str = xstat.mode_str(meta.mode)
941 symlink_target = meta.symlink_target
942 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
943 mtime_str = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
944 if meta.user and not numeric_ids:
946 elif meta.uid != None:
947 user_str = str(meta.uid)
948 if meta.group and not numeric_ids:
949 group_str = meta.group
950 elif meta.gid != None:
951 group_str = str(meta.gid)
952 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
954 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
956 elif meta.size != None:
958 size_or_dev_str = format_filesize(meta.size)
960 size_or_dev_str = str(meta.size)
962 size_or_dev_str = '-'
964 classification_str = \
965 xstat.classification_str(meta.mode, classification == 'all')
968 mtime_str = '????-??-?? ??:??'
969 classification_str = '?'
973 name += classification_str
975 name += ' -> ' + meta.symlink_target
977 return '%-10s %-11s %11s %16s %s' % (mode_str,
978 user_str + "/" + group_str,
984 def detailed_str(meta, fields = None):
985 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
986 # 0", "link-target:", etc.
992 path = meta.path or ''
993 result.append('path: ' + path)
995 result.append('mode: %s (%s)' % (oct(meta.mode),
996 xstat.mode_str(meta.mode)))
997 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
998 result.append('link-target: ' + meta.symlink_target)
1001 result.append('rdev: %d,%d' % (os.major(meta.rdev),
1002 os.minor(meta.rdev)))
1004 result.append('rdev: 0')
1005 if 'size' in fields and meta.size:
1006 result.append('size: ' + str(meta.size))
1008 result.append('uid: ' + str(meta.uid))
1010 result.append('gid: ' + str(meta.gid))
1011 if 'user' in fields:
1012 result.append('user: ' + meta.user)
1013 if 'group' in fields:
1014 result.append('group: ' + meta.group)
1015 if 'atime' in fields:
1016 # If we don't have xstat.lutime, that means we have to use
1017 # utime(), and utime() has no way to set the mtime/atime of a
1018 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1019 # so let's not report it. (That way scripts comparing
1020 # before/after won't trigger.)
1021 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1022 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1024 result.append('atime: 0')
1025 if 'mtime' in fields:
1026 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1027 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1029 result.append('mtime: 0')
1030 if 'ctime' in fields:
1031 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1032 if 'linux-attr' in fields and meta.linux_attr:
1033 result.append('linux-attr: ' + hex(meta.linux_attr))
1034 if 'linux-xattr' in fields and meta.linux_xattr:
1035 for name, value in meta.linux_xattr:
1036 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1037 if 'posix1e-acl' in fields and meta.posix1e_acl:
1038 acl = meta.posix1e_acl[0]
1039 result.append('posix1e-acl: ' + acl + '\n')
1040 if stat.S_ISDIR(meta.mode):
1041 def_acl = meta.posix1e_acl[2]
1042 result.append('posix1e-acl-default: ' + def_acl + '\n')
1043 return '\n'.join(result)
1046 class _ArchiveIterator:
1049 return Metadata.read(self._file)
1051 raise StopIteration()
1056 def __init__(self, file):
1060 def display_archive(file):
1063 for meta in _ArchiveIterator(file):
1066 print detailed_str(meta)
1069 for meta in _ArchiveIterator(file):
1070 print summary_str(meta)
1072 for meta in _ArchiveIterator(file):
1074 print >> sys.stderr, \
1075 'bup: no metadata path, but asked to only display path', \
1076 '(increase verbosity?)'
1081 def start_extract(file, create_symlinks=True):
1082 for meta in _ArchiveIterator(file):
1083 if not meta: # Hit end record.
1086 print >> sys.stderr, meta.path
1087 xpath = _clean_up_extract_path(meta.path)
1089 add_error(Exception('skipping risky path "%s"' % meta.path))
1092 _set_up_path(meta, create_symlinks=create_symlinks)
1095 def finish_extract(file, restore_numeric_ids=False):
1097 for meta in _ArchiveIterator(file):
1098 if not meta: # Hit end record.
1100 xpath = _clean_up_extract_path(meta.path)
1102 add_error(Exception('skipping risky path "%s"' % dir.path))
1104 if os.path.isdir(meta.path):
1105 all_dirs.append(meta)
1108 print >> sys.stderr, meta.path
1109 meta.apply_to_path(path=xpath,
1110 restore_numeric_ids=restore_numeric_ids)
1111 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1112 for dir in all_dirs:
1113 # Don't need to check xpath -- won't be in all_dirs if not OK.
1114 xpath = _clean_up_extract_path(dir.path)
1116 print >> sys.stderr, dir.path
1117 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1120 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1121 # For now, just store all the directories and handle them last,
1124 for meta in _ArchiveIterator(file):
1125 if not meta: # Hit end record.
1127 xpath = _clean_up_extract_path(meta.path)
1129 add_error(Exception('skipping risky path "%s"' % meta.path))
1133 print >> sys.stderr, '+', meta.path
1134 _set_up_path(meta, create_symlinks=create_symlinks)
1135 if os.path.isdir(meta.path):
1136 all_dirs.append(meta)
1139 print >> sys.stderr, '=', meta.path
1140 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1141 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1142 for dir in all_dirs:
1143 # Don't need to check xpath -- won't be in all_dirs if not OK.
1144 xpath = _clean_up_extract_path(dir.path)
1146 print >> sys.stderr, '=', xpath
1147 # Shouldn't have to check for risky paths here (omitted above).
1148 dir.apply_to_path(path=dir.path,
1149 restore_numeric_ids=restore_numeric_ids)