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 from time import gmtime, strftime
11 import errno, os, sys, stat, time, pwd, grp, socket, struct
13 from bup import vint, xstat
14 from bup.drecurse import recursive_dirlist
15 from bup.helpers import add_error, mkdirp, log, is_superuser, format_filesize
16 from bup.helpers import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
17 from bup.xstat import utime, lutime
20 if sys.platform.startswith('linux'):
24 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
28 except AttributeError:
29 log('Warning: python-xattr module is too old; '
30 'install python-pyxattr instead.\n')
34 if not (sys.platform.startswith('cygwin') \
35 or sys.platform.startswith('darwin') \
36 or sys.platform.startswith('netbsd')):
40 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
43 from bup._helpers import get_linux_file_attr, set_linux_file_attr
45 # No need for a warning here; the only reason they won't exist is that we're
46 # not on Linux, in which case files don't have any linux attrs anyway, so
47 # lacking the functions isn't a problem.
48 get_linux_file_attr = set_linux_file_attr = None
51 # See the bup_get_linux_file_attr() comments.
52 _suppress_linux_file_attr = \
53 sys.byteorder == 'big' and struct.calcsize('@l') > struct.calcsize('@i')
55 def check_linux_file_attr_api():
56 global get_linux_file_attr, set_linux_file_attr
57 if not (get_linux_file_attr or set_linux_file_attr):
59 if _suppress_linux_file_attr:
60 log('Warning: Linux attr support disabled (see "bup help index").\n')
61 get_linux_file_attr = set_linux_file_attr = None
64 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
66 # Q: Consider hardlink support?
67 # Q: Is it OK to store raw linux attr (chattr) flags?
68 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
69 # Q: Is the application of posix1e has_extended() correct?
70 # Q: Is one global --numeric-ids argument sufficient?
71 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
72 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
74 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
75 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
76 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
77 # FIXME: Consider pack('vvvvsss', ...) optimization.
81 # osx (varies between hfs and hfs+):
82 # type - regular dir char block fifo socket ...
83 # perms - rwxrwxrwxsgt
84 # times - ctime atime mtime
87 # hard-link-info (hfs+ only)
90 # attributes-osx see chflags
96 # type - regular dir ...
97 # times - creation, modification, posix change, access
100 # attributes - see attrib
102 # forks (alternate data streams)
106 # type - regular dir ...
107 # perms - rwxrwxrwx (maybe - see wikipedia)
108 # times - creation, modification, access
109 # attributes - see attrib
113 _have_lchmod = hasattr(os, 'lchmod')
116 def _clean_up_path_for_archive(p):
117 # Not the most efficient approach.
120 # Take everything after any '/../'.
121 pos = result.rfind('/../')
123 result = result[result.rfind('/../') + 4:]
125 # Take everything after any remaining '../'.
126 if result.startswith("../"):
129 # Remove any '/./' sequences.
130 pos = result.find('/./')
132 result = result[0:pos] + '/' + result[pos + 3:]
133 pos = result.find('/./')
135 # Remove any leading '/'s.
136 result = result.lstrip('/')
138 # Replace '//' with '/' everywhere.
139 pos = result.find('//')
141 result = result[0:pos] + '/' + result[pos + 2:]
142 pos = result.find('//')
144 # Take everything after any remaining './'.
145 if result.startswith('./'):
148 # Take everything before any remaining '/.'.
149 if result.endswith('/.'):
152 if result == '' or result.endswith('/..'):
159 if p.startswith('/'):
161 if p.find('/../') != -1:
163 if p.startswith('../'):
165 if p.endswith('/..'):
170 def _clean_up_extract_path(p):
171 result = p.lstrip('/')
174 elif _risky_path(result):
180 # These tags are currently conceptually private to Metadata, and they
181 # must be unique, and must *never* be changed.
184 _rec_tag_common = 2 # times, user, group, type, perms, etc. (legacy/broken)
185 _rec_tag_symlink_target = 3
186 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
187 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
188 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
189 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
190 _rec_tag_hardlink_target = 8 # hard link target path
191 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
193 _warned_about_attr_einval = None
196 class ApplyError(Exception):
197 # Thrown when unable to apply any given bit of metadata to a path.
202 # Metadata is stored as a sequence of tagged binary records. Each
203 # record will have some subset of add, encode, load, create, and
204 # apply methods, i.e. _add_foo...
206 # We do allow an "empty" object as a special case, i.e. no
207 # records. One can be created by trying to write Metadata(), and
208 # for such an object, read() will return None. This is used by
209 # "bup save", for example, as a placeholder in cases where
212 # NOTE: if any relevant fields are added or removed, be sure to
213 # update same_file() below.
217 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
218 # must be non-negative and < 10**9.
220 def _add_common(self, path, st):
221 assert(st.st_uid >= 0)
222 assert(st.st_gid >= 0)
225 self.atime = st.st_atime
226 self.mtime = st.st_mtime
227 self.ctime = st.st_ctime
228 self.user = self.group = ''
229 entry = pwd_from_uid(st.st_uid)
231 self.user = entry.pw_name
232 entry = grp_from_gid(st.st_gid)
234 self.group = entry.gr_name
235 self.mode = st.st_mode
236 # Only collect st_rdev if we might need it for a mknod()
237 # during restore. On some platforms (i.e. kFreeBSD), it isn't
238 # stable for other file types. For example "cp -a" will
239 # change it for a plain file.
240 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
241 self.rdev = st.st_rdev
245 def _same_common(self, other):
246 """Return true or false to indicate similarity in the hardlink sense."""
247 return self.uid == other.uid \
248 and self.gid == other.gid \
249 and self.rdev == other.rdev \
250 and self.mtime == other.mtime \
251 and self.ctime == other.ctime \
252 and self.user == other.user \
253 and self.group == other.group
255 def _encode_common(self):
258 atime = xstat.nsecs_to_timespec(self.atime)
259 mtime = xstat.nsecs_to_timespec(self.mtime)
260 ctime = xstat.nsecs_to_timespec(self.ctime)
261 result = vint.pack('vvsvsvvVvVvV',
276 def _load_common_rec(self, port, legacy_format=False):
277 unpack_fmt = 'vvsvsvvVvVvV'
279 unpack_fmt = 'VVsVsVvVvVvV'
280 data = vint.read_bvec(port)
292 ctime_ns) = vint.unpack(unpack_fmt, data)
293 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
294 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
295 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
297 def _recognized_file_type(self):
298 return stat.S_ISREG(self.mode) \
299 or stat.S_ISDIR(self.mode) \
300 or stat.S_ISCHR(self.mode) \
301 or stat.S_ISBLK(self.mode) \
302 or stat.S_ISFIFO(self.mode) \
303 or stat.S_ISSOCK(self.mode) \
304 or stat.S_ISLNK(self.mode)
306 def _create_via_common_rec(self, path, create_symlinks=True):
308 raise ApplyError('no metadata - cannot create path ' + path)
310 # If the path already exists and is a dir, try rmdir.
311 # If the path already exists and is anything else, try unlink.
314 st = xstat.lstat(path)
316 if e.errno != errno.ENOENT:
319 if stat.S_ISDIR(st.st_mode):
323 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
324 msg = 'refusing to overwrite non-empty dir ' + path
330 if stat.S_ISREG(self.mode):
331 assert(self._recognized_file_type())
332 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
334 elif stat.S_ISDIR(self.mode):
335 assert(self._recognized_file_type())
336 os.mkdir(path, 0o700)
337 elif stat.S_ISCHR(self.mode):
338 assert(self._recognized_file_type())
339 os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
340 elif stat.S_ISBLK(self.mode):
341 assert(self._recognized_file_type())
342 os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
343 elif stat.S_ISFIFO(self.mode):
344 assert(self._recognized_file_type())
345 os.mknod(path, 0o600 | stat.S_IFIFO)
346 elif stat.S_ISSOCK(self.mode):
348 os.mknod(path, 0o600 | stat.S_IFSOCK)
350 if e.errno in (errno.EINVAL, errno.EPERM):
351 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
355 elif stat.S_ISLNK(self.mode):
356 assert(self._recognized_file_type())
357 if self.symlink_target and create_symlinks:
358 # on MacOS, symlink() permissions depend on umask, and there's
359 # no way to chown a symlink after creating it, so we have to
361 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
363 os.symlink(self.symlink_target, path)
366 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
368 assert(not self._recognized_file_type())
369 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
372 def _apply_common_rec(self, path, restore_numeric_ids=False):
374 raise ApplyError('no metadata - cannot apply to ' + path)
376 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
377 # EACCES errors at this stage are fatal for the current path.
378 if lutime and stat.S_ISLNK(self.mode):
380 lutime(path, (self.atime, self.mtime))
382 if e.errno == errno.EACCES:
383 raise ApplyError('lutime: %s' % e)
388 utime(path, (self.atime, self.mtime))
390 if e.errno == errno.EACCES:
391 raise ApplyError('utime: %s' % e)
395 uid = gid = -1 # By default, do nothing.
399 if not restore_numeric_ids:
400 if self.uid != 0 and self.user:
401 entry = pwd_from_name(self.user)
404 if self.gid != 0 and self.group:
405 entry = grp_from_name(self.group)
408 else: # not superuser - only consider changing the group/gid
409 user_gids = os.getgroups()
410 if self.gid in user_gids:
412 if not restore_numeric_ids and self.gid != 0:
413 # The grp might not exist on the local system.
414 grps = filter(None, [grp_from_gid(x) for x in user_gids])
415 if self.group in [x.gr_name for x in grps]:
416 g = grp_from_name(self.group)
420 if uid != -1 or gid != -1:
422 os.lchown(path, uid, gid)
424 if e.errno == errno.EPERM:
425 add_error('lchown: %s' % e)
426 elif sys.platform.startswith('cygwin') \
427 and e.errno == errno.EINVAL:
428 add_error('lchown: unknown uid/gid (%d/%d) for %s'
435 os.lchmod(path, stat.S_IMODE(self.mode))
436 except errno.ENOSYS: # Function not implemented
438 elif not stat.S_ISLNK(self.mode):
439 os.chmod(path, stat.S_IMODE(self.mode))
444 def _encode_path(self):
446 return vint.pack('s', self.path)
450 def _load_path_rec(self, port):
451 self.path = vint.unpack('s', vint.read_bvec(port))[0]
456 def _add_symlink_target(self, path, st):
458 if stat.S_ISLNK(st.st_mode):
459 self.symlink_target = os.readlink(path)
461 add_error('readlink: %s' % e)
463 def _encode_symlink_target(self):
464 return self.symlink_target
466 def _load_symlink_target_rec(self, port):
467 target = vint.read_bvec(port)
468 self.symlink_target = target
469 self.size = len(target)
474 def _add_hardlink_target(self, target):
475 self.hardlink_target = target
477 def _same_hardlink_target(self, other):
478 """Return true or false to indicate similarity in the hardlink sense."""
479 return self.hardlink_target == other.hardlink_target
481 def _encode_hardlink_target(self):
482 return self.hardlink_target
484 def _load_hardlink_target_rec(self, port):
485 self.hardlink_target = vint.read_bvec(port)
488 ## POSIX1e ACL records
490 # Recorded as a list:
491 # [txt_id_acl, num_id_acl]
492 # or, if a directory:
493 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
494 # The numeric/text distinction only matters when reading/restoring
496 def _add_posix1e_acl(self, path, st):
497 if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
499 if not stat.S_ISLNK(st.st_mode):
503 if posix1e.has_extended(path):
504 acl = posix1e.ACL(file=path)
505 acls = [acl, acl] # txt and num are the same
506 if stat.S_ISDIR(st.st_mode):
507 def_acl = posix1e.ACL(filedef=path)
508 def_acls = [def_acl, def_acl]
509 except EnvironmentError as e:
510 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
513 txt_flags = posix1e.TEXT_ABBREVIATE
514 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
515 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
516 acls[1].to_any_text('', '\n', num_flags)]
518 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
519 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
520 self.posix1e_acl = acl_rep
522 def _same_posix1e_acl(self, other):
523 """Return true or false to indicate similarity in the hardlink sense."""
524 return self.posix1e_acl == other.posix1e_acl
526 def _encode_posix1e_acl(self):
527 # Encode as two strings (w/default ACL string possibly empty).
529 acls = self.posix1e_acl
531 acls.extend(['', ''])
532 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
536 def _load_posix1e_acl_rec(self, port):
537 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
539 acl_rep = acl_rep[:2]
540 self.posix1e_acl = acl_rep
542 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
543 def apply_acl(acl_rep, kind):
545 acl = posix1e.ACL(text = acl_rep)
548 # pylibacl appears to return an IOError with errno
549 # set to 0 if a group referred to by the ACL rep
550 # doesn't exist on the current system.
551 raise ApplyError("POSIX1e ACL: can't create %r for %r"
556 acl.applyto(path, kind)
558 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
559 raise ApplyError('POSIX1e ACL applyto: %s' % e)
565 add_error("%s: can't restore ACLs; posix1e support missing.\n"
569 acls = self.posix1e_acl
571 if restore_numeric_ids:
572 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
574 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
575 if restore_numeric_ids:
576 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
578 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
581 ## Linux attributes (lsattr(1), chattr(1))
583 def _add_linux_attr(self, path, st):
584 check_linux_file_attr_api()
585 if not get_linux_file_attr: return
586 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
588 attr = get_linux_file_attr(path)
590 self.linux_attr = attr
592 if e.errno == errno.EACCES:
593 add_error('read Linux attr: %s' % e)
594 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
595 # Assume filesystem doesn't support attrs.
597 elif e.errno == EINVAL:
598 global _warned_about_attr_einval
599 if not _warned_about_attr_einval:
600 log("Ignoring attr EINVAL;"
601 + " if you're not using ntfs-3g, please report: "
603 _warned_about_attr_einval = True
608 def _same_linux_attr(self, other):
609 """Return true or false to indicate similarity in the hardlink sense."""
610 return self.linux_attr == other.linux_attr
612 def _encode_linux_attr(self):
614 return vint.pack('V', self.linux_attr)
618 def _load_linux_attr_rec(self, port):
619 data = vint.read_bvec(port)
620 self.linux_attr = vint.unpack('V', data)[0]
622 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
624 check_linux_file_attr_api()
625 if not set_linux_file_attr:
626 add_error("%s: can't restore linuxattrs: "
627 "linuxattr support missing.\n" % path)
630 set_linux_file_attr(path, self.linux_attr)
632 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
633 raise ApplyError('Linux chattr: %s (0x%s)'
634 % (e, hex(self.linux_attr)))
635 elif e.errno == EINVAL:
636 msg = "if you're not using ntfs-3g, please report"
637 raise ApplyError('Linux chattr: %s (0x%s) (%s)'
638 % (e, hex(self.linux_attr), msg))
643 ## Linux extended attributes (getfattr(1), setfattr(1))
645 def _add_linux_xattr(self, path, st):
648 self.linux_xattr = xattr.get_all(path, nofollow=True)
649 except EnvironmentError as e:
650 if e.errno != errno.EOPNOTSUPP:
653 def _same_linux_xattr(self, other):
654 """Return true or false to indicate similarity in the hardlink sense."""
655 return self.linux_xattr == other.linux_xattr
657 def _encode_linux_xattr(self):
659 result = vint.pack('V', len(self.linux_xattr))
660 for name, value in self.linux_xattr:
661 result += vint.pack('ss', name, value)
666 def _load_linux_xattr_rec(self, file):
667 data = vint.read_bvec(file)
668 memfile = BytesIO(data)
670 for i in range(vint.read_vuint(memfile)):
671 key = vint.read_bvec(memfile)
672 value = vint.read_bvec(memfile)
673 result.append((key, value))
674 self.linux_xattr = result
676 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
679 add_error("%s: can't restore xattr; xattr support missing.\n"
682 if not self.linux_xattr:
685 existing_xattrs = set(xattr.list(path, nofollow=True))
687 if e.errno == errno.EACCES:
688 raise ApplyError('xattr.set %r: %s' % (path, e))
691 for k, v in self.linux_xattr:
692 if k not in existing_xattrs \
693 or v != xattr.get(path, k, nofollow=True):
695 xattr.set(path, k, v, nofollow=True)
697 if e.errno == errno.EPERM \
698 or e.errno == errno.EOPNOTSUPP:
699 raise ApplyError('xattr.set %r: %s' % (path, e))
702 existing_xattrs -= frozenset([k])
703 for k in existing_xattrs:
705 xattr.remove(path, k, nofollow=True)
707 if e.errno in (errno.EPERM, errno.EACCES):
708 raise ApplyError('xattr.remove %r: %s' % (path, e))
713 self.mode = self.uid = self.gid = self.user = self.group = None
714 self.atime = self.mtime = self.ctime = None
718 self.symlink_target = None
719 self.hardlink_target = None
720 self.linux_attr = None
721 self.linux_xattr = None
722 self.posix1e_acl = None
725 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
726 if self.path is not None:
727 result += ' path:' + repr(self.path)
728 if self.mode is not None:
729 result += ' mode:' + repr(xstat.mode_str(self.mode)
730 + '(%s)' % hex(self.mode))
731 if self.uid is not None:
732 result += ' uid:' + str(self.uid)
733 if self.gid is not None:
734 result += ' gid:' + str(self.gid)
735 if self.user is not None:
736 result += ' user:' + repr(self.user)
737 if self.group is not None:
738 result += ' group:' + repr(self.group)
739 if self.size is not None:
740 result += ' size:' + repr(self.size)
741 for name, val in (('atime', self.atime),
742 ('mtime', self.mtime),
743 ('ctime', self.ctime)):
745 result += ' %s:%r (%d)' \
747 strftime('%Y-%m-%d %H:%M %z',
748 gmtime(xstat.fstime_floor_secs(val))),
751 return ''.join(result)
753 def write(self, port, include_path=True):
754 records = include_path and [(_rec_tag_path, self._encode_path())] or []
755 records.extend([(_rec_tag_common_v2, self._encode_common()),
756 (_rec_tag_symlink_target,
757 self._encode_symlink_target()),
758 (_rec_tag_hardlink_target,
759 self._encode_hardlink_target()),
760 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
761 (_rec_tag_linux_attr, self._encode_linux_attr()),
762 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
763 for tag, data in records:
765 vint.write_vuint(port, tag)
766 vint.write_bvec(port, data)
767 vint.write_vuint(port, _rec_tag_end)
769 def encode(self, include_path=True):
771 self.write(port, include_path)
772 return port.getvalue()
776 # This method should either return a valid Metadata object,
777 # return None if there was no information at all (just a
778 # _rec_tag_end), throw EOFError if there was nothing at all to
779 # read, or throw an Exception if a valid object could not be
781 tag = vint.read_vuint(port)
782 if tag == _rec_tag_end:
784 try: # From here on, EOF is an error.
786 while True: # only exit is error (exception) or _rec_tag_end
787 if tag == _rec_tag_path:
788 result._load_path_rec(port)
789 elif tag == _rec_tag_common_v2:
790 result._load_common_rec(port)
791 elif tag == _rec_tag_symlink_target:
792 result._load_symlink_target_rec(port)
793 elif tag == _rec_tag_hardlink_target:
794 result._load_hardlink_target_rec(port)
795 elif tag == _rec_tag_posix1e_acl:
796 result._load_posix1e_acl_rec(port)
797 elif tag == _rec_tag_linux_attr:
798 result._load_linux_attr_rec(port)
799 elif tag == _rec_tag_linux_xattr:
800 result._load_linux_xattr_rec(port)
801 elif tag == _rec_tag_end:
803 elif tag == _rec_tag_common: # Should be very rare.
804 result._load_common_rec(port, legacy_format = True)
805 else: # unknown record
807 tag = vint.read_vuint(port)
809 raise Exception("EOF while reading Metadata")
812 return stat.S_ISDIR(self.mode)
814 def create_path(self, path, create_symlinks=True):
815 self._create_via_common_rec(path, create_symlinks=create_symlinks)
817 def apply_to_path(self, path=None, restore_numeric_ids=False):
818 # apply metadata to path -- file must exist
822 raise Exception('Metadata.apply_to_path() called with no path')
823 if not self._recognized_file_type():
824 add_error('not applying metadata to "%s"' % path
825 + ' with unrecognized mode "0x%x"\n' % self.mode)
827 num_ids = restore_numeric_ids
828 for apply_metadata in (self._apply_common_rec,
829 self._apply_posix1e_acl_rec,
830 self._apply_linux_attr_rec,
831 self._apply_linux_xattr_rec):
833 apply_metadata(path, restore_numeric_ids=num_ids)
834 except ApplyError as e:
837 def same_file(self, other):
838 """Compare this to other for equivalency. Return true if
839 their information implies they could represent the same file
840 on disk, in the hardlink sense. Assume they're both regular
842 return self._same_common(other) \
843 and self._same_hardlink_target(other) \
844 and self._same_posix1e_acl(other) \
845 and self._same_linux_attr(other) \
846 and self._same_linux_xattr(other)
849 def from_path(path, statinfo=None, archive_path=None,
850 save_symlinks=True, hardlink_target=None):
852 result.path = archive_path
853 st = statinfo or xstat.lstat(path)
854 result.size = st.st_size
855 result._add_common(path, st)
857 result._add_symlink_target(path, st)
858 result._add_hardlink_target(hardlink_target)
859 result._add_posix1e_acl(path, st)
860 result._add_linux_attr(path, st)
861 result._add_linux_xattr(path, st)
865 def save_tree(output_file, paths,
871 # Issue top-level rewrite warnings.
873 safe_path = _clean_up_path_for_archive(path)
874 if safe_path != path:
875 log('archiving "%s" as "%s"\n' % (path, safe_path))
879 safe_path = _clean_up_path_for_archive(p)
881 if stat.S_ISDIR(st.st_mode):
883 m = from_path(p, statinfo=st, archive_path=safe_path,
884 save_symlinks=save_symlinks)
886 print >> sys.stderr, m.path
887 m.write(output_file, include_path=write_paths)
889 start_dir = os.getcwd()
891 for (p, st) in recursive_dirlist(paths, xdev=xdev):
892 dirlist_dir = os.getcwd()
894 safe_path = _clean_up_path_for_archive(p)
895 m = from_path(p, statinfo=st, archive_path=safe_path,
896 save_symlinks=save_symlinks)
898 print >> sys.stderr, m.path
899 m.write(output_file, include_path=write_paths)
900 os.chdir(dirlist_dir)
905 def _set_up_path(meta, create_symlinks=True):
906 # Allow directories to exist as a special case -- might have
907 # been created by an earlier longer path.
911 parent = os.path.dirname(meta.path)
914 meta.create_path(meta.path, create_symlinks=create_symlinks)
917 all_fields = frozenset(['path',
934 def summary_str(meta, numeric_ids = False, classification = None,
935 human_readable = False):
937 """Return a string containing the "ls -l" style listing for meta.
938 Classification may be "all", "type", or None."""
939 user_str = group_str = size_or_dev_str = '?'
940 symlink_target = None
943 mode_str = xstat.mode_str(meta.mode)
944 symlink_target = meta.symlink_target
945 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
946 mtime_str = strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
947 if meta.user and not numeric_ids:
949 elif meta.uid != None:
950 user_str = str(meta.uid)
951 if meta.group and not numeric_ids:
952 group_str = meta.group
953 elif meta.gid != None:
954 group_str = str(meta.gid)
955 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
957 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
959 elif meta.size != None:
961 size_or_dev_str = format_filesize(meta.size)
963 size_or_dev_str = str(meta.size)
965 size_or_dev_str = '-'
967 classification_str = \
968 xstat.classification_str(meta.mode, classification == 'all')
971 mtime_str = '????-??-?? ??:??'
972 classification_str = '?'
976 name += classification_str
978 name += ' -> ' + meta.symlink_target
980 return '%-10s %-11s %11s %16s %s' % (mode_str,
981 user_str + "/" + group_str,
987 def detailed_str(meta, fields = None):
988 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
989 # 0", "link-target:", etc.
995 path = meta.path or ''
996 result.append('path: ' + path)
998 result.append('mode: %s (%s)' % (oct(meta.mode),
999 xstat.mode_str(meta.mode)))
1000 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1001 result.append('link-target: ' + meta.symlink_target)
1002 if 'rdev' in fields:
1004 result.append('rdev: %d,%d' % (os.major(meta.rdev),
1005 os.minor(meta.rdev)))
1007 result.append('rdev: 0')
1008 if 'size' in fields and meta.size:
1009 result.append('size: ' + str(meta.size))
1011 result.append('uid: ' + str(meta.uid))
1013 result.append('gid: ' + str(meta.gid))
1014 if 'user' in fields:
1015 result.append('user: ' + meta.user)
1016 if 'group' in fields:
1017 result.append('group: ' + meta.group)
1018 if 'atime' in fields:
1019 # If we don't have xstat.lutime, that means we have to use
1020 # utime(), and utime() has no way to set the mtime/atime of a
1021 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1022 # so let's not report it. (That way scripts comparing
1023 # before/after won't trigger.)
1024 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1025 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1027 result.append('atime: 0')
1028 if 'mtime' in fields:
1029 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1030 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1032 result.append('mtime: 0')
1033 if 'ctime' in fields:
1034 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1035 if 'linux-attr' in fields and meta.linux_attr:
1036 result.append('linux-attr: ' + hex(meta.linux_attr))
1037 if 'linux-xattr' in fields and meta.linux_xattr:
1038 for name, value in meta.linux_xattr:
1039 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1040 if 'posix1e-acl' in fields and meta.posix1e_acl:
1041 acl = meta.posix1e_acl[0]
1042 result.append('posix1e-acl: ' + acl + '\n')
1043 if stat.S_ISDIR(meta.mode):
1044 def_acl = meta.posix1e_acl[2]
1045 result.append('posix1e-acl-default: ' + def_acl + '\n')
1046 return '\n'.join(result)
1049 class _ArchiveIterator:
1052 return Metadata.read(self._file)
1054 raise StopIteration()
1059 def __init__(self, file):
1063 def display_archive(file):
1066 for meta in _ArchiveIterator(file):
1069 print detailed_str(meta)
1072 for meta in _ArchiveIterator(file):
1073 print summary_str(meta)
1075 for meta in _ArchiveIterator(file):
1077 print >> sys.stderr, \
1078 'bup: no metadata path, but asked to only display path', \
1079 '(increase verbosity?)'
1084 def start_extract(file, create_symlinks=True):
1085 for meta in _ArchiveIterator(file):
1086 if not meta: # Hit end record.
1089 print >> sys.stderr, meta.path
1090 xpath = _clean_up_extract_path(meta.path)
1092 add_error(Exception('skipping risky path "%s"' % meta.path))
1095 _set_up_path(meta, create_symlinks=create_symlinks)
1098 def finish_extract(file, restore_numeric_ids=False):
1100 for meta in _ArchiveIterator(file):
1101 if not meta: # Hit end record.
1103 xpath = _clean_up_extract_path(meta.path)
1105 add_error(Exception('skipping risky path "%s"' % dir.path))
1107 if os.path.isdir(meta.path):
1108 all_dirs.append(meta)
1111 print >> sys.stderr, meta.path
1112 meta.apply_to_path(path=xpath,
1113 restore_numeric_ids=restore_numeric_ids)
1114 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1115 for dir in all_dirs:
1116 # Don't need to check xpath -- won't be in all_dirs if not OK.
1117 xpath = _clean_up_extract_path(dir.path)
1119 print >> sys.stderr, dir.path
1120 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1123 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1124 # For now, just store all the directories and handle them last,
1127 for meta in _ArchiveIterator(file):
1128 if not meta: # Hit end record.
1130 xpath = _clean_up_extract_path(meta.path)
1132 add_error(Exception('skipping risky path "%s"' % meta.path))
1136 print >> sys.stderr, '+', meta.path
1137 _set_up_path(meta, create_symlinks=create_symlinks)
1138 if os.path.isdir(meta.path):
1139 all_dirs.append(meta)
1142 print >> sys.stderr, '=', meta.path
1143 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1144 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1145 for dir in all_dirs:
1146 # Don't need to check xpath -- won't be in all_dirs if not OK.
1147 xpath = _clean_up_extract_path(dir.path)
1149 print >> sys.stderr, '=', xpath
1150 # Shouldn't have to check for risky paths here (omitted above).
1151 dir.apply_to_path(path=dir.path,
1152 restore_numeric_ids=restore_numeric_ids)