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 self.symlink_target = vint.read_bvec(port)
471 def _add_hardlink_target(self, target):
472 self.hardlink_target = target
474 def _same_hardlink_target(self, other):
475 """Return true or false to indicate similarity in the hardlink sense."""
476 return self.hardlink_target == other.hardlink_target
478 def _encode_hardlink_target(self):
479 return self.hardlink_target
481 def _load_hardlink_target_rec(self, port):
482 self.hardlink_target = vint.read_bvec(port)
485 ## POSIX1e ACL records
487 # Recorded as a list:
488 # [txt_id_acl, num_id_acl]
489 # or, if a directory:
490 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
491 # The numeric/text distinction only matters when reading/restoring
493 def _add_posix1e_acl(self, path, st):
494 if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
496 if not stat.S_ISLNK(st.st_mode):
500 if posix1e.has_extended(path):
501 acl = posix1e.ACL(file=path)
502 acls = [acl, acl] # txt and num are the same
503 if stat.S_ISDIR(st.st_mode):
504 def_acl = posix1e.ACL(filedef=path)
505 def_acls = [def_acl, def_acl]
506 except EnvironmentError as e:
507 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
510 txt_flags = posix1e.TEXT_ABBREVIATE
511 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
512 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
513 acls[1].to_any_text('', '\n', num_flags)]
515 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
516 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
517 self.posix1e_acl = acl_rep
519 def _same_posix1e_acl(self, other):
520 """Return true or false to indicate similarity in the hardlink sense."""
521 return self.posix1e_acl == other.posix1e_acl
523 def _encode_posix1e_acl(self):
524 # Encode as two strings (w/default ACL string possibly empty).
526 acls = self.posix1e_acl
528 acls.extend(['', ''])
529 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
533 def _load_posix1e_acl_rec(self, port):
534 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
536 acl_rep = acl_rep[:2]
537 self.posix1e_acl = acl_rep
539 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
540 def apply_acl(acl_rep, kind):
542 acl = posix1e.ACL(text = acl_rep)
545 # pylibacl appears to return an IOError with errno
546 # set to 0 if a group referred to by the ACL rep
547 # doesn't exist on the current system.
548 raise ApplyError("POSIX1e ACL: can't create %r for %r"
553 acl.applyto(path, kind)
555 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
556 raise ApplyError('POSIX1e ACL applyto: %s' % e)
562 add_error("%s: can't restore ACLs; posix1e support missing.\n"
566 acls = self.posix1e_acl
568 if restore_numeric_ids:
569 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
571 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
572 if restore_numeric_ids:
573 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
575 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
578 ## Linux attributes (lsattr(1), chattr(1))
580 def _add_linux_attr(self, path, st):
581 check_linux_file_attr_api()
582 if not get_linux_file_attr: return
583 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
585 attr = get_linux_file_attr(path)
587 self.linux_attr = attr
589 if e.errno == errno.EACCES:
590 add_error('read Linux attr: %s' % e)
591 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
592 # Assume filesystem doesn't support attrs.
594 elif e.errno == EINVAL:
595 global _warned_about_attr_einval
596 if not _warned_about_attr_einval:
597 log("Ignoring attr EINVAL;"
598 + " if you're not using ntfs-3g, please report: "
600 _warned_about_attr_einval = True
605 def _same_linux_attr(self, other):
606 """Return true or false to indicate similarity in the hardlink sense."""
607 return self.linux_attr == other.linux_attr
609 def _encode_linux_attr(self):
611 return vint.pack('V', self.linux_attr)
615 def _load_linux_attr_rec(self, port):
616 data = vint.read_bvec(port)
617 self.linux_attr = vint.unpack('V', data)[0]
619 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
621 check_linux_file_attr_api()
622 if not set_linux_file_attr:
623 add_error("%s: can't restore linuxattrs: "
624 "linuxattr support missing.\n" % path)
627 set_linux_file_attr(path, self.linux_attr)
629 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
630 raise ApplyError('Linux chattr: %s (0x%s)'
631 % (e, hex(self.linux_attr)))
632 elif e.errno == EINVAL:
633 msg = "if you're not using ntfs-3g, please report"
634 raise ApplyError('Linux chattr: %s (0x%s) (%s)'
635 % (e, hex(self.linux_attr), msg))
640 ## Linux extended attributes (getfattr(1), setfattr(1))
642 def _add_linux_xattr(self, path, st):
645 self.linux_xattr = xattr.get_all(path, nofollow=True)
646 except EnvironmentError as e:
647 if e.errno != errno.EOPNOTSUPP:
650 def _same_linux_xattr(self, other):
651 """Return true or false to indicate similarity in the hardlink sense."""
652 return self.linux_xattr == other.linux_xattr
654 def _encode_linux_xattr(self):
656 result = vint.pack('V', len(self.linux_xattr))
657 for name, value in self.linux_xattr:
658 result += vint.pack('ss', name, value)
663 def _load_linux_xattr_rec(self, file):
664 data = vint.read_bvec(file)
665 memfile = BytesIO(data)
667 for i in range(vint.read_vuint(memfile)):
668 key = vint.read_bvec(memfile)
669 value = vint.read_bvec(memfile)
670 result.append((key, value))
671 self.linux_xattr = result
673 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
676 add_error("%s: can't restore xattr; xattr support missing.\n"
679 if not self.linux_xattr:
682 existing_xattrs = set(xattr.list(path, nofollow=True))
684 if e.errno == errno.EACCES:
685 raise ApplyError('xattr.set %r: %s' % (path, e))
688 for k, v in self.linux_xattr:
689 if k not in existing_xattrs \
690 or v != xattr.get(path, k, nofollow=True):
692 xattr.set(path, k, v, nofollow=True)
694 if e.errno == errno.EPERM \
695 or e.errno == errno.EOPNOTSUPP:
696 raise ApplyError('xattr.set %r: %s' % (path, e))
699 existing_xattrs -= frozenset([k])
700 for k in existing_xattrs:
702 xattr.remove(path, k, nofollow=True)
704 if e.errno in (errno.EPERM, errno.EACCES):
705 raise ApplyError('xattr.remove %r: %s' % (path, e))
710 self.mode = self.uid = self.gid = self.user = self.group = None
711 self.atime = self.mtime = self.ctime = None
715 self.symlink_target = None
716 self.hardlink_target = None
717 self.linux_attr = None
718 self.linux_xattr = None
719 self.posix1e_acl = None
722 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
724 result += ' path:' + repr(self.path)
726 result += ' mode:' + repr(xstat.mode_str(self.mode)
727 + '(%s)' % hex(self.mode))
729 result += ' uid:' + str(self.uid)
731 result += ' gid:' + str(self.gid)
733 result += ' user:' + repr(self.user)
735 result += ' group:' + repr(self.group)
737 result += ' size:' + repr(self.size)
738 for name, val in (('atime', self.atime),
739 ('mtime', self.mtime),
740 ('ctime', self.ctime)):
743 time.strftime('%Y-%m-%d %H:%M %z',
744 time.gmtime(xstat.fstime_floor_secs(val))))
746 return ''.join(result)
748 def write(self, port, include_path=True):
749 records = include_path and [(_rec_tag_path, self._encode_path())] or []
750 records.extend([(_rec_tag_common_v2, self._encode_common()),
751 (_rec_tag_symlink_target,
752 self._encode_symlink_target()),
753 (_rec_tag_hardlink_target,
754 self._encode_hardlink_target()),
755 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
756 (_rec_tag_linux_attr, self._encode_linux_attr()),
757 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
758 for tag, data in records:
760 vint.write_vuint(port, tag)
761 vint.write_bvec(port, data)
762 vint.write_vuint(port, _rec_tag_end)
764 def encode(self, include_path=True):
766 self.write(port, include_path)
767 return port.getvalue()
771 # This method should either return a valid Metadata object,
772 # return None if there was no information at all (just a
773 # _rec_tag_end), throw EOFError if there was nothing at all to
774 # read, or throw an Exception if a valid object could not be
776 tag = vint.read_vuint(port)
777 if tag == _rec_tag_end:
779 try: # From here on, EOF is an error.
781 while True: # only exit is error (exception) or _rec_tag_end
782 if tag == _rec_tag_path:
783 result._load_path_rec(port)
784 elif tag == _rec_tag_common_v2:
785 result._load_common_rec(port)
786 elif tag == _rec_tag_symlink_target:
787 result._load_symlink_target_rec(port)
788 elif tag == _rec_tag_hardlink_target:
789 result._load_hardlink_target_rec(port)
790 elif tag == _rec_tag_posix1e_acl:
791 result._load_posix1e_acl_rec(port)
792 elif tag == _rec_tag_linux_attr:
793 result._load_linux_attr_rec(port)
794 elif tag == _rec_tag_linux_xattr:
795 result._load_linux_xattr_rec(port)
796 elif tag == _rec_tag_end:
798 elif tag == _rec_tag_common: # Should be very rare.
799 result._load_common_rec(port, legacy_format = True)
800 else: # unknown record
802 tag = vint.read_vuint(port)
804 raise Exception("EOF while reading Metadata")
807 return stat.S_ISDIR(self.mode)
809 def create_path(self, path, create_symlinks=True):
810 self._create_via_common_rec(path, create_symlinks=create_symlinks)
812 def apply_to_path(self, path=None, restore_numeric_ids=False):
813 # apply metadata to path -- file must exist
817 raise Exception('Metadata.apply_to_path() called with no path')
818 if not self._recognized_file_type():
819 add_error('not applying metadata to "%s"' % path
820 + ' with unrecognized mode "0x%x"\n' % self.mode)
822 num_ids = restore_numeric_ids
823 for apply_metadata in (self._apply_common_rec,
824 self._apply_posix1e_acl_rec,
825 self._apply_linux_attr_rec,
826 self._apply_linux_xattr_rec):
828 apply_metadata(path, restore_numeric_ids=num_ids)
829 except ApplyError as e:
832 def same_file(self, other):
833 """Compare this to other for equivalency. Return true if
834 their information implies they could represent the same file
835 on disk, in the hardlink sense. Assume they're both regular
837 return self._same_common(other) \
838 and self._same_hardlink_target(other) \
839 and self._same_posix1e_acl(other) \
840 and self._same_linux_attr(other) \
841 and self._same_linux_xattr(other)
844 def from_path(path, statinfo=None, archive_path=None,
845 save_symlinks=True, hardlink_target=None):
847 result.path = archive_path
848 st = statinfo or xstat.lstat(path)
849 result.size = st.st_size
850 result._add_common(path, st)
852 result._add_symlink_target(path, st)
853 result._add_hardlink_target(hardlink_target)
854 result._add_posix1e_acl(path, st)
855 result._add_linux_attr(path, st)
856 result._add_linux_xattr(path, st)
860 def save_tree(output_file, paths,
866 # Issue top-level rewrite warnings.
868 safe_path = _clean_up_path_for_archive(path)
869 if safe_path != path:
870 log('archiving "%s" as "%s"\n' % (path, safe_path))
874 safe_path = _clean_up_path_for_archive(p)
876 if stat.S_ISDIR(st.st_mode):
878 m = from_path(p, statinfo=st, archive_path=safe_path,
879 save_symlinks=save_symlinks)
881 print >> sys.stderr, m.path
882 m.write(output_file, include_path=write_paths)
884 start_dir = os.getcwd()
886 for (p, st) in recursive_dirlist(paths, xdev=xdev):
887 dirlist_dir = os.getcwd()
889 safe_path = _clean_up_path_for_archive(p)
890 m = from_path(p, statinfo=st, archive_path=safe_path,
891 save_symlinks=save_symlinks)
893 print >> sys.stderr, m.path
894 m.write(output_file, include_path=write_paths)
895 os.chdir(dirlist_dir)
900 def _set_up_path(meta, create_symlinks=True):
901 # Allow directories to exist as a special case -- might have
902 # been created by an earlier longer path.
906 parent = os.path.dirname(meta.path)
909 meta.create_path(meta.path, create_symlinks=create_symlinks)
912 all_fields = frozenset(['path',
929 def summary_str(meta, numeric_ids = False, classification = None,
930 human_readable = False):
932 """Return a string containing the "ls -l" style listing for meta.
933 Classification may be "all", "type", or None."""
934 user_str = group_str = size_or_dev_str = '?'
935 symlink_target = None
938 mode_str = xstat.mode_str(meta.mode)
939 symlink_target = meta.symlink_target
940 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
941 mtime_str = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
942 if meta.user and not numeric_ids:
944 elif meta.uid != None:
945 user_str = str(meta.uid)
946 if meta.group and not numeric_ids:
947 group_str = meta.group
948 elif meta.gid != None:
949 group_str = str(meta.gid)
950 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
952 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
954 elif meta.size != None:
956 size_or_dev_str = format_filesize(meta.size)
958 size_or_dev_str = str(meta.size)
960 size_or_dev_str = '-'
962 classification_str = \
963 xstat.classification_str(meta.mode, classification == 'all')
966 mtime_str = '????-??-?? ??:??'
967 classification_str = '?'
971 name += classification_str
973 name += ' -> ' + meta.symlink_target
975 return '%-10s %-11s %11s %16s %s' % (mode_str,
976 user_str + "/" + group_str,
982 def detailed_str(meta, fields = None):
983 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
984 # 0", "link-target:", etc.
990 path = meta.path or ''
991 result.append('path: ' + path)
993 result.append('mode: %s (%s)' % (oct(meta.mode),
994 xstat.mode_str(meta.mode)))
995 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
996 result.append('link-target: ' + meta.symlink_target)
999 result.append('rdev: %d,%d' % (os.major(meta.rdev),
1000 os.minor(meta.rdev)))
1002 result.append('rdev: 0')
1003 if 'size' in fields and meta.size:
1004 result.append('size: ' + str(meta.size))
1006 result.append('uid: ' + str(meta.uid))
1008 result.append('gid: ' + str(meta.gid))
1009 if 'user' in fields:
1010 result.append('user: ' + meta.user)
1011 if 'group' in fields:
1012 result.append('group: ' + meta.group)
1013 if 'atime' in fields:
1014 # If we don't have xstat.lutime, that means we have to use
1015 # utime(), and utime() has no way to set the mtime/atime of a
1016 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1017 # so let's not report it. (That way scripts comparing
1018 # before/after won't trigger.)
1019 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1020 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1022 result.append('atime: 0')
1023 if 'mtime' in fields:
1024 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1025 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1027 result.append('mtime: 0')
1028 if 'ctime' in fields:
1029 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1030 if 'linux-attr' in fields and meta.linux_attr:
1031 result.append('linux-attr: ' + hex(meta.linux_attr))
1032 if 'linux-xattr' in fields and meta.linux_xattr:
1033 for name, value in meta.linux_xattr:
1034 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1035 if 'posix1e-acl' in fields and meta.posix1e_acl:
1036 acl = meta.posix1e_acl[0]
1037 result.append('posix1e-acl: ' + acl + '\n')
1038 if stat.S_ISDIR(meta.mode):
1039 def_acl = meta.posix1e_acl[2]
1040 result.append('posix1e-acl-default: ' + def_acl + '\n')
1041 return '\n'.join(result)
1044 class _ArchiveIterator:
1047 return Metadata.read(self._file)
1049 raise StopIteration()
1054 def __init__(self, file):
1058 def display_archive(file):
1061 for meta in _ArchiveIterator(file):
1064 print detailed_str(meta)
1067 for meta in _ArchiveIterator(file):
1068 print summary_str(meta)
1070 for meta in _ArchiveIterator(file):
1072 print >> sys.stderr, \
1073 'bup: no metadata path, but asked to only display path', \
1074 '(increase verbosity?)'
1079 def start_extract(file, create_symlinks=True):
1080 for meta in _ArchiveIterator(file):
1081 if not meta: # Hit end record.
1084 print >> sys.stderr, meta.path
1085 xpath = _clean_up_extract_path(meta.path)
1087 add_error(Exception('skipping risky path "%s"' % meta.path))
1090 _set_up_path(meta, create_symlinks=create_symlinks)
1093 def finish_extract(file, restore_numeric_ids=False):
1095 for meta in _ArchiveIterator(file):
1096 if not meta: # Hit end record.
1098 xpath = _clean_up_extract_path(meta.path)
1100 add_error(Exception('skipping risky path "%s"' % dir.path))
1102 if os.path.isdir(meta.path):
1103 all_dirs.append(meta)
1106 print >> sys.stderr, meta.path
1107 meta.apply_to_path(path=xpath,
1108 restore_numeric_ids=restore_numeric_ids)
1109 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1110 for dir in all_dirs:
1111 # Don't need to check xpath -- won't be in all_dirs if not OK.
1112 xpath = _clean_up_extract_path(dir.path)
1114 print >> sys.stderr, dir.path
1115 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1118 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1119 # For now, just store all the directories and handle them last,
1122 for meta in _ArchiveIterator(file):
1123 if not meta: # Hit end record.
1125 xpath = _clean_up_extract_path(meta.path)
1127 add_error(Exception('skipping risky path "%s"' % meta.path))
1131 print >> sys.stderr, '+', meta.path
1132 _set_up_path(meta, create_symlinks=create_symlinks)
1133 if os.path.isdir(meta.path):
1134 all_dirs.append(meta)
1137 print >> sys.stderr, '=', meta.path
1138 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1139 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1140 for dir in all_dirs:
1141 # Don't need to check xpath -- won't be in all_dirs if not OK.
1142 xpath = _clean_up_extract_path(dir.path)
1144 print >> sys.stderr, '=', xpath
1145 # Shouldn't have to check for risky paths here (omitted above).
1146 dir.apply_to_path(path=dir.path,
1147 restore_numeric_ids=restore_numeric_ids)