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.
9 import errno, os, sys, stat, time, pwd, grp, socket, struct
11 from bup import vint, xstat
12 from bup.drecurse import recursive_dirlist
13 from bup.helpers import add_error, mkdirp, log, is_superuser, format_filesize
14 from bup.helpers import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
15 from bup.xstat import utime, lutime
18 if sys.platform.startswith('linux'):
22 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
26 except AttributeError:
27 log('Warning: python-xattr module is too old; '
28 'install python-pyxattr instead.\n')
32 if not (sys.platform.startswith('cygwin') \
33 or sys.platform.startswith('darwin') \
34 or sys.platform.startswith('netbsd')):
38 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
41 from bup._helpers import get_linux_file_attr, set_linux_file_attr
43 # No need for a warning here; the only reason they won't exist is that we're
44 # not on Linux, in which case files don't have any linux attrs anyway, so
45 # lacking the functions isn't a problem.
46 get_linux_file_attr = set_linux_file_attr = None
49 # See the bup_get_linux_file_attr() comments.
50 _suppress_linux_file_attr = \
51 sys.byteorder == 'big' and struct.calcsize('@l') > struct.calcsize('@i')
53 def check_linux_file_attr_api():
54 global get_linux_file_attr, set_linux_file_attr
55 if not (get_linux_file_attr or set_linux_file_attr):
57 if _suppress_linux_file_attr:
58 log('Warning: Linux attr support disabled (see "bup help index").\n')
59 get_linux_file_attr = set_linux_file_attr = None
62 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
64 # Q: Consider hardlink support?
65 # Q: Is it OK to store raw linux attr (chattr) flags?
66 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
67 # Q: Is the application of posix1e has_extended() correct?
68 # Q: Is one global --numeric-ids argument sufficient?
69 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
70 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
72 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
73 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
74 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
75 # FIXME: Consider pack('vvvvsss', ...) optimization.
79 # osx (varies between hfs and hfs+):
80 # type - regular dir char block fifo socket ...
81 # perms - rwxrwxrwxsgt
82 # times - ctime atime mtime
85 # hard-link-info (hfs+ only)
88 # attributes-osx see chflags
94 # type - regular dir ...
95 # times - creation, modification, posix change, access
98 # attributes - see attrib
100 # forks (alternate data streams)
104 # type - regular dir ...
105 # perms - rwxrwxrwx (maybe - see wikipedia)
106 # times - creation, modification, access
107 # attributes - see attrib
111 _have_lchmod = hasattr(os, 'lchmod')
114 def _clean_up_path_for_archive(p):
115 # Not the most efficient approach.
118 # Take everything after any '/../'.
119 pos = result.rfind('/../')
121 result = result[result.rfind('/../') + 4:]
123 # Take everything after any remaining '../'.
124 if result.startswith("../"):
127 # Remove any '/./' sequences.
128 pos = result.find('/./')
130 result = result[0:pos] + '/' + result[pos + 3:]
131 pos = result.find('/./')
133 # Remove any leading '/'s.
134 result = result.lstrip('/')
136 # Replace '//' with '/' everywhere.
137 pos = result.find('//')
139 result = result[0:pos] + '/' + result[pos + 2:]
140 pos = result.find('//')
142 # Take everything after any remaining './'.
143 if result.startswith('./'):
146 # Take everything before any remaining '/.'.
147 if result.endswith('/.'):
150 if result == '' or result.endswith('/..'):
157 if p.startswith('/'):
159 if p.find('/../') != -1:
161 if p.startswith('../'):
163 if p.endswith('/..'):
168 def _clean_up_extract_path(p):
169 result = p.lstrip('/')
172 elif _risky_path(result):
178 # These tags are currently conceptually private to Metadata, and they
179 # must be unique, and must *never* be changed.
182 _rec_tag_common = 2 # times, user, group, type, perms, etc. (legacy/broken)
183 _rec_tag_symlink_target = 3
184 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
185 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
186 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
187 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
188 _rec_tag_hardlink_target = 8 # hard link target path
189 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
192 class ApplyError(Exception):
193 # Thrown when unable to apply any given bit of metadata to a path.
198 # Metadata is stored as a sequence of tagged binary records. Each
199 # record will have some subset of add, encode, load, create, and
200 # apply methods, i.e. _add_foo...
202 # We do allow an "empty" object as a special case, i.e. no
203 # records. One can be created by trying to write Metadata(), and
204 # for such an object, read() will return None. This is used by
205 # "bup save", for example, as a placeholder in cases where
208 # NOTE: if any relevant fields are added or removed, be sure to
209 # update same_file() below.
213 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
214 # must be non-negative and < 10**9.
216 def _add_common(self, path, st):
217 assert(st.st_uid >= 0)
218 assert(st.st_gid >= 0)
221 self.atime = st.st_atime
222 self.mtime = st.st_mtime
223 self.ctime = st.st_ctime
224 self.user = self.group = ''
225 entry = pwd_from_uid(st.st_uid)
227 self.user = entry.pw_name
228 entry = grp_from_gid(st.st_gid)
230 self.group = entry.gr_name
231 self.mode = st.st_mode
232 # Only collect st_rdev if we might need it for a mknod()
233 # during restore. On some platforms (i.e. kFreeBSD), it isn't
234 # stable for other file types. For example "cp -a" will
235 # change it for a plain file.
236 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
237 self.rdev = st.st_rdev
241 def _same_common(self, other):
242 """Return true or false to indicate similarity in the hardlink sense."""
243 return self.uid == other.uid \
244 and self.gid == other.gid \
245 and self.rdev == other.rdev \
246 and self.mtime == other.mtime \
247 and self.ctime == other.ctime \
248 and self.user == other.user \
249 and self.group == other.group
251 def _encode_common(self):
254 atime = xstat.nsecs_to_timespec(self.atime)
255 mtime = xstat.nsecs_to_timespec(self.mtime)
256 ctime = xstat.nsecs_to_timespec(self.ctime)
257 result = vint.pack('vvsvsvvVvVvV',
272 def _load_common_rec(self, port, legacy_format=False):
273 unpack_fmt = 'vvsvsvvVvVvV'
275 unpack_fmt = 'VVsVsVvVvVvV'
276 data = vint.read_bvec(port)
288 ctime_ns) = vint.unpack(unpack_fmt, data)
289 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
290 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
291 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
293 def _recognized_file_type(self):
294 return stat.S_ISREG(self.mode) \
295 or stat.S_ISDIR(self.mode) \
296 or stat.S_ISCHR(self.mode) \
297 or stat.S_ISBLK(self.mode) \
298 or stat.S_ISFIFO(self.mode) \
299 or stat.S_ISSOCK(self.mode) \
300 or stat.S_ISLNK(self.mode)
302 def _create_via_common_rec(self, path, create_symlinks=True):
304 raise ApplyError('no metadata - cannot create path ' + path)
306 # If the path already exists and is a dir, try rmdir.
307 # If the path already exists and is anything else, try unlink.
310 st = xstat.lstat(path)
312 if e.errno != errno.ENOENT:
315 if stat.S_ISDIR(st.st_mode):
319 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
320 msg = 'refusing to overwrite non-empty dir ' + path
326 if stat.S_ISREG(self.mode):
327 assert(self._recognized_file_type())
328 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
330 elif stat.S_ISDIR(self.mode):
331 assert(self._recognized_file_type())
332 os.mkdir(path, 0o700)
333 elif stat.S_ISCHR(self.mode):
334 assert(self._recognized_file_type())
335 os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
336 elif stat.S_ISBLK(self.mode):
337 assert(self._recognized_file_type())
338 os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
339 elif stat.S_ISFIFO(self.mode):
340 assert(self._recognized_file_type())
341 os.mknod(path, 0o600 | stat.S_IFIFO)
342 elif stat.S_ISSOCK(self.mode):
344 os.mknod(path, 0o600 | stat.S_IFSOCK)
346 if e.errno in (errno.EINVAL, errno.EPERM):
347 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
351 elif stat.S_ISLNK(self.mode):
352 assert(self._recognized_file_type())
353 if self.symlink_target and create_symlinks:
354 # on MacOS, symlink() permissions depend on umask, and there's
355 # no way to chown a symlink after creating it, so we have to
357 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
359 os.symlink(self.symlink_target, path)
362 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
364 assert(not self._recognized_file_type())
365 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
368 def _apply_common_rec(self, path, restore_numeric_ids=False):
370 raise ApplyError('no metadata - cannot apply to ' + path)
372 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
373 # EACCES errors at this stage are fatal for the current path.
374 if lutime and stat.S_ISLNK(self.mode):
376 lutime(path, (self.atime, self.mtime))
378 if e.errno == errno.EACCES:
379 raise ApplyError('lutime: %s' % e)
384 utime(path, (self.atime, self.mtime))
386 if e.errno == errno.EACCES:
387 raise ApplyError('utime: %s' % e)
391 uid = gid = -1 # By default, do nothing.
395 if not restore_numeric_ids:
396 if self.uid != 0 and self.user:
397 entry = pwd_from_name(self.user)
400 if self.gid != 0 and self.group:
401 entry = grp_from_name(self.group)
404 else: # not superuser - only consider changing the group/gid
405 user_gids = os.getgroups()
406 if self.gid in user_gids:
408 if not restore_numeric_ids and self.gid != 0:
409 # The grp might not exist on the local system.
410 grps = filter(None, [grp_from_gid(x) for x in user_gids])
411 if self.group in [x.gr_name for x in grps]:
412 g = grp_from_name(self.group)
416 if uid != -1 or gid != -1:
418 os.lchown(path, uid, gid)
420 if e.errno == errno.EPERM:
421 add_error('lchown: %s' % e)
422 elif sys.platform.startswith('cygwin') \
423 and e.errno == errno.EINVAL:
424 add_error('lchown: unknown uid/gid (%d/%d) for %s'
431 os.lchmod(path, stat.S_IMODE(self.mode))
432 except errno.ENOSYS: # Function not implemented
434 elif not stat.S_ISLNK(self.mode):
435 os.chmod(path, stat.S_IMODE(self.mode))
440 def _encode_path(self):
442 return vint.pack('s', self.path)
446 def _load_path_rec(self, port):
447 self.path = vint.unpack('s', vint.read_bvec(port))[0]
452 def _add_symlink_target(self, path, st):
454 if stat.S_ISLNK(st.st_mode):
455 self.symlink_target = os.readlink(path)
457 add_error('readlink: %s' % e)
459 def _encode_symlink_target(self):
460 return self.symlink_target
462 def _load_symlink_target_rec(self, port):
463 self.symlink_target = vint.read_bvec(port)
468 def _add_hardlink_target(self, target):
469 self.hardlink_target = target
471 def _same_hardlink_target(self, other):
472 """Return true or false to indicate similarity in the hardlink sense."""
473 return self.hardlink_target == other.hardlink_target
475 def _encode_hardlink_target(self):
476 return self.hardlink_target
478 def _load_hardlink_target_rec(self, port):
479 self.hardlink_target = vint.read_bvec(port)
482 ## POSIX1e ACL records
484 # Recorded as a list:
485 # [txt_id_acl, num_id_acl]
486 # or, if a directory:
487 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
488 # The numeric/text distinction only matters when reading/restoring
490 def _add_posix1e_acl(self, path, st):
491 if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
493 if not stat.S_ISLNK(st.st_mode):
497 if posix1e.has_extended(path):
498 acl = posix1e.ACL(file=path)
499 acls = [acl, acl] # txt and num are the same
500 if stat.S_ISDIR(st.st_mode):
501 def_acl = posix1e.ACL(filedef=path)
502 def_acls = [def_acl, def_acl]
503 except EnvironmentError as e:
504 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
507 txt_flags = posix1e.TEXT_ABBREVIATE
508 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
509 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
510 acls[1].to_any_text('', '\n', num_flags)]
512 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
513 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
514 self.posix1e_acl = acl_rep
516 def _same_posix1e_acl(self, other):
517 """Return true or false to indicate similarity in the hardlink sense."""
518 return self.posix1e_acl == other.posix1e_acl
520 def _encode_posix1e_acl(self):
521 # Encode as two strings (w/default ACL string possibly empty).
523 acls = self.posix1e_acl
525 acls.extend(['', ''])
526 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
530 def _load_posix1e_acl_rec(self, port):
531 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
533 acl_rep = acl_rep[:2]
534 self.posix1e_acl = acl_rep
536 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
537 def apply_acl(acl_rep, kind):
539 acl = posix1e.ACL(text = acl_rep)
542 # pylibacl appears to return an IOError with errno
543 # set to 0 if a group referred to by the ACL rep
544 # doesn't exist on the current system.
545 raise ApplyError("POSIX1e ACL: can't create %r for %r"
550 acl.applyto(path, kind)
552 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
553 raise ApplyError('POSIX1e ACL applyto: %s' % e)
559 add_error("%s: can't restore ACLs; posix1e support missing.\n"
563 acls = self.posix1e_acl
565 if restore_numeric_ids:
566 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
568 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
569 if restore_numeric_ids:
570 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
572 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
575 ## Linux attributes (lsattr(1), chattr(1))
577 def _add_linux_attr(self, path, st):
578 check_linux_file_attr_api()
579 if not get_linux_file_attr: return
580 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
582 attr = get_linux_file_attr(path)
584 self.linux_attr = attr
586 if e.errno == errno.EACCES:
587 add_error('read Linux attr: %s' % e)
588 elif e.errno in (errno.ENOTTY, errno.ENOSYS, errno.EOPNOTSUPP):
589 # Assume filesystem doesn't support attrs.
594 def _same_linux_attr(self, other):
595 """Return true or false to indicate similarity in the hardlink sense."""
596 return self.linux_attr == other.linux_attr
598 def _encode_linux_attr(self):
600 return vint.pack('V', self.linux_attr)
604 def _load_linux_attr_rec(self, port):
605 data = vint.read_bvec(port)
606 self.linux_attr = vint.unpack('V', data)[0]
608 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
610 check_linux_file_attr_api()
611 if not set_linux_file_attr:
612 add_error("%s: can't restore linuxattrs: "
613 "linuxattr support missing.\n" % path)
616 set_linux_file_attr(path, self.linux_attr)
618 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS,
620 raise ApplyError('Linux chattr: %s (0x%s)'
621 % (e, hex(self.linux_attr)))
626 ## Linux extended attributes (getfattr(1), setfattr(1))
628 def _add_linux_xattr(self, path, st):
631 self.linux_xattr = xattr.get_all(path, nofollow=True)
632 except EnvironmentError as e:
633 if e.errno != errno.EOPNOTSUPP:
636 def _same_linux_xattr(self, other):
637 """Return true or false to indicate similarity in the hardlink sense."""
638 return self.linux_xattr == other.linux_xattr
640 def _encode_linux_xattr(self):
642 result = vint.pack('V', len(self.linux_xattr))
643 for name, value in self.linux_xattr:
644 result += vint.pack('ss', name, value)
649 def _load_linux_xattr_rec(self, file):
650 data = vint.read_bvec(file)
651 memfile = BytesIO(data)
653 for i in range(vint.read_vuint(memfile)):
654 key = vint.read_bvec(memfile)
655 value = vint.read_bvec(memfile)
656 result.append((key, value))
657 self.linux_xattr = result
659 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
662 add_error("%s: can't restore xattr; xattr support missing.\n"
665 if not self.linux_xattr:
668 existing_xattrs = set(xattr.list(path, nofollow=True))
670 if e.errno == errno.EACCES:
671 raise ApplyError('xattr.set %r: %s' % (path, e))
674 for k, v in self.linux_xattr:
675 if k not in existing_xattrs \
676 or v != xattr.get(path, k, nofollow=True):
678 xattr.set(path, k, v, nofollow=True)
680 if e.errno == errno.EPERM \
681 or e.errno == errno.EOPNOTSUPP:
682 raise ApplyError('xattr.set %r: %s' % (path, e))
685 existing_xattrs -= frozenset([k])
686 for k in existing_xattrs:
688 xattr.remove(path, k, nofollow=True)
690 if e.errno in (errno.EPERM, errno.EACCES):
691 raise ApplyError('xattr.remove %r: %s' % (path, e))
696 self.mode = self.uid = self.gid = self.user = self.group = None
697 self.atime = self.mtime = self.ctime = None
701 self.symlink_target = None
702 self.hardlink_target = None
703 self.linux_attr = None
704 self.linux_xattr = None
705 self.posix1e_acl = None
708 result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
710 result += ' path:' + repr(self.path)
712 result += ' mode:' + repr(xstat.mode_str(self.mode)
713 + '(%s)' % hex(self.mode))
715 result += ' uid:' + str(self.uid)
717 result += ' gid:' + str(self.gid)
719 result += ' user:' + repr(self.user)
721 result += ' group:' + repr(self.group)
723 result += ' size:' + repr(self.size)
724 for name, val in (('atime', self.atime),
725 ('mtime', self.mtime),
726 ('ctime', self.ctime)):
729 time.strftime('%Y-%m-%d %H:%M %z',
730 time.gmtime(xstat.fstime_floor_secs(val))))
732 return ''.join(result)
734 def write(self, port, include_path=True):
735 records = include_path and [(_rec_tag_path, self._encode_path())] or []
736 records.extend([(_rec_tag_common_v2, self._encode_common()),
737 (_rec_tag_symlink_target,
738 self._encode_symlink_target()),
739 (_rec_tag_hardlink_target,
740 self._encode_hardlink_target()),
741 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
742 (_rec_tag_linux_attr, self._encode_linux_attr()),
743 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
744 for tag, data in records:
746 vint.write_vuint(port, tag)
747 vint.write_bvec(port, data)
748 vint.write_vuint(port, _rec_tag_end)
750 def encode(self, include_path=True):
752 self.write(port, include_path)
753 return port.getvalue()
757 # This method should either return a valid Metadata object,
758 # return None if there was no information at all (just a
759 # _rec_tag_end), throw EOFError if there was nothing at all to
760 # read, or throw an Exception if a valid object could not be
762 tag = vint.read_vuint(port)
763 if tag == _rec_tag_end:
765 try: # From here on, EOF is an error.
767 while True: # only exit is error (exception) or _rec_tag_end
768 if tag == _rec_tag_path:
769 result._load_path_rec(port)
770 elif tag == _rec_tag_common_v2:
771 result._load_common_rec(port)
772 elif tag == _rec_tag_symlink_target:
773 result._load_symlink_target_rec(port)
774 elif tag == _rec_tag_hardlink_target:
775 result._load_hardlink_target_rec(port)
776 elif tag == _rec_tag_posix1e_acl:
777 result._load_posix1e_acl_rec(port)
778 elif tag == _rec_tag_linux_attr:
779 result._load_linux_attr_rec(port)
780 elif tag == _rec_tag_linux_xattr:
781 result._load_linux_xattr_rec(port)
782 elif tag == _rec_tag_end:
784 elif tag == _rec_tag_common: # Should be very rare.
785 result._load_common_rec(port, legacy_format = True)
786 else: # unknown record
788 tag = vint.read_vuint(port)
790 raise Exception("EOF while reading Metadata")
793 return stat.S_ISDIR(self.mode)
795 def create_path(self, path, create_symlinks=True):
796 self._create_via_common_rec(path, create_symlinks=create_symlinks)
798 def apply_to_path(self, path=None, restore_numeric_ids=False):
799 # apply metadata to path -- file must exist
803 raise Exception('Metadata.apply_to_path() called with no path')
804 if not self._recognized_file_type():
805 add_error('not applying metadata to "%s"' % path
806 + ' with unrecognized mode "0x%x"\n' % self.mode)
808 num_ids = restore_numeric_ids
809 for apply_metadata in (self._apply_common_rec,
810 self._apply_posix1e_acl_rec,
811 self._apply_linux_attr_rec,
812 self._apply_linux_xattr_rec):
814 apply_metadata(path, restore_numeric_ids=num_ids)
815 except ApplyError as e:
818 def same_file(self, other):
819 """Compare this to other for equivalency. Return true if
820 their information implies they could represent the same file
821 on disk, in the hardlink sense. Assume they're both regular
823 return self._same_common(other) \
824 and self._same_hardlink_target(other) \
825 and self._same_posix1e_acl(other) \
826 and self._same_linux_attr(other) \
827 and self._same_linux_xattr(other)
830 def from_path(path, statinfo=None, archive_path=None,
831 save_symlinks=True, hardlink_target=None):
833 result.path = archive_path
834 st = statinfo or xstat.lstat(path)
835 result.size = st.st_size
836 result._add_common(path, st)
838 result._add_symlink_target(path, st)
839 result._add_hardlink_target(hardlink_target)
840 result._add_posix1e_acl(path, st)
841 result._add_linux_attr(path, st)
842 result._add_linux_xattr(path, st)
846 def save_tree(output_file, paths,
852 # Issue top-level rewrite warnings.
854 safe_path = _clean_up_path_for_archive(path)
855 if safe_path != path:
856 log('archiving "%s" as "%s"\n' % (path, safe_path))
860 safe_path = _clean_up_path_for_archive(p)
862 if stat.S_ISDIR(st.st_mode):
864 m = from_path(p, statinfo=st, archive_path=safe_path,
865 save_symlinks=save_symlinks)
867 print >> sys.stderr, m.path
868 m.write(output_file, include_path=write_paths)
870 start_dir = os.getcwd()
872 for (p, st) in recursive_dirlist(paths, xdev=xdev):
873 dirlist_dir = os.getcwd()
875 safe_path = _clean_up_path_for_archive(p)
876 m = from_path(p, statinfo=st, archive_path=safe_path,
877 save_symlinks=save_symlinks)
879 print >> sys.stderr, m.path
880 m.write(output_file, include_path=write_paths)
881 os.chdir(dirlist_dir)
886 def _set_up_path(meta, create_symlinks=True):
887 # Allow directories to exist as a special case -- might have
888 # been created by an earlier longer path.
892 parent = os.path.dirname(meta.path)
895 meta.create_path(meta.path, create_symlinks=create_symlinks)
898 all_fields = frozenset(['path',
915 def summary_str(meta, numeric_ids = False, classification = None,
916 human_readable = False):
918 """Return a string containing the "ls -l" style listing for meta.
919 Classification may be "all", "type", or None."""
920 user_str = group_str = size_or_dev_str = '?'
921 symlink_target = None
924 mode_str = xstat.mode_str(meta.mode)
925 symlink_target = meta.symlink_target
926 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
927 mtime_str = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
928 if meta.user and not numeric_ids:
930 elif meta.uid != None:
931 user_str = str(meta.uid)
932 if meta.group and not numeric_ids:
933 group_str = meta.group
934 elif meta.gid != None:
935 group_str = str(meta.gid)
936 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
938 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
940 elif meta.size != None:
942 size_or_dev_str = format_filesize(meta.size)
944 size_or_dev_str = str(meta.size)
946 size_or_dev_str = '-'
948 classification_str = \
949 xstat.classification_str(meta.mode, classification == 'all')
952 mtime_str = '????-??-?? ??:??'
953 classification_str = '?'
957 name += classification_str
959 name += ' -> ' + meta.symlink_target
961 return '%-10s %-11s %11s %16s %s' % (mode_str,
962 user_str + "/" + group_str,
968 def detailed_str(meta, fields = None):
969 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
970 # 0", "link-target:", etc.
976 path = meta.path or ''
977 result.append('path: ' + path)
979 result.append('mode: %s (%s)' % (oct(meta.mode),
980 xstat.mode_str(meta.mode)))
981 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
982 result.append('link-target: ' + meta.symlink_target)
985 result.append('rdev: %d,%d' % (os.major(meta.rdev),
986 os.minor(meta.rdev)))
988 result.append('rdev: 0')
989 if 'size' in fields and meta.size:
990 result.append('size: ' + str(meta.size))
992 result.append('uid: ' + str(meta.uid))
994 result.append('gid: ' + str(meta.gid))
996 result.append('user: ' + meta.user)
997 if 'group' in fields:
998 result.append('group: ' + meta.group)
999 if 'atime' in fields:
1000 # If we don't have xstat.lutime, that means we have to use
1001 # utime(), and utime() has no way to set the mtime/atime of a
1002 # symlink. Thus, the mtime/atime of a symlink is meaningless,
1003 # so let's not report it. (That way scripts comparing
1004 # before/after won't trigger.)
1005 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1006 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1008 result.append('atime: 0')
1009 if 'mtime' in fields:
1010 if xstat.lutime or not stat.S_ISLNK(meta.mode):
1011 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1013 result.append('mtime: 0')
1014 if 'ctime' in fields:
1015 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1016 if 'linux-attr' in fields and meta.linux_attr:
1017 result.append('linux-attr: ' + hex(meta.linux_attr))
1018 if 'linux-xattr' in fields and meta.linux_xattr:
1019 for name, value in meta.linux_xattr:
1020 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1021 if 'posix1e-acl' in fields and meta.posix1e_acl:
1022 acl = meta.posix1e_acl[0]
1023 result.append('posix1e-acl: ' + acl + '\n')
1024 if stat.S_ISDIR(meta.mode):
1025 def_acl = meta.posix1e_acl[2]
1026 result.append('posix1e-acl-default: ' + def_acl + '\n')
1027 return '\n'.join(result)
1030 class _ArchiveIterator:
1033 return Metadata.read(self._file)
1035 raise StopIteration()
1040 def __init__(self, file):
1044 def display_archive(file):
1047 for meta in _ArchiveIterator(file):
1050 print detailed_str(meta)
1053 for meta in _ArchiveIterator(file):
1054 print summary_str(meta)
1056 for meta in _ArchiveIterator(file):
1058 print >> sys.stderr, \
1059 'bup: no metadata path, but asked to only display path', \
1060 '(increase verbosity?)'
1065 def start_extract(file, create_symlinks=True):
1066 for meta in _ArchiveIterator(file):
1067 if not meta: # Hit end record.
1070 print >> sys.stderr, meta.path
1071 xpath = _clean_up_extract_path(meta.path)
1073 add_error(Exception('skipping risky path "%s"' % meta.path))
1076 _set_up_path(meta, create_symlinks=create_symlinks)
1079 def finish_extract(file, restore_numeric_ids=False):
1081 for meta in _ArchiveIterator(file):
1082 if not meta: # Hit end record.
1084 xpath = _clean_up_extract_path(meta.path)
1086 add_error(Exception('skipping risky path "%s"' % dir.path))
1088 if os.path.isdir(meta.path):
1089 all_dirs.append(meta)
1092 print >> sys.stderr, meta.path
1093 meta.apply_to_path(path=xpath,
1094 restore_numeric_ids=restore_numeric_ids)
1095 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1096 for dir in all_dirs:
1097 # Don't need to check xpath -- won't be in all_dirs if not OK.
1098 xpath = _clean_up_extract_path(dir.path)
1100 print >> sys.stderr, dir.path
1101 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1104 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1105 # For now, just store all the directories and handle them last,
1108 for meta in _ArchiveIterator(file):
1109 if not meta: # Hit end record.
1111 xpath = _clean_up_extract_path(meta.path)
1113 add_error(Exception('skipping risky path "%s"' % meta.path))
1117 print >> sys.stderr, '+', meta.path
1118 _set_up_path(meta, create_symlinks=create_symlinks)
1119 if os.path.isdir(meta.path):
1120 all_dirs.append(meta)
1123 print >> sys.stderr, '=', meta.path
1124 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1125 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1126 for dir in all_dirs:
1127 # Don't need to check xpath -- won't be in all_dirs if not OK.
1128 xpath = _clean_up_extract_path(dir.path)
1130 print >> sys.stderr, '=', xpath
1131 # Shouldn't have to check for risky paths here (omitted above).
1132 dir.apply_to_path(path=dir.path,
1133 restore_numeric_ids=restore_numeric_ids)