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.
7 import errno, os, sys, stat, time, pwd, grp, socket
8 from cStringIO import StringIO
9 from bup import vint, xstat
10 from bup.drecurse import recursive_dirlist
11 from bup.helpers import add_error, mkdirp, log, is_superuser
12 from bup.helpers import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
13 from bup.xstat import utime, lutime
16 if sys.platform.startswith('linux'):
20 log('Warning: Linux xattr support missing; install python-pyxattr.\n')
24 except AttributeError:
25 log('Warning: python-xattr module is too old; '
26 'install python-pyxattr instead.\n')
30 if not (sys.platform.startswith('cygwin') or sys.platform.startswith('darwin')):
34 log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
37 from bup._helpers import get_linux_file_attr, set_linux_file_attr
39 # No need for a warning here; the only reason they won't exist is that we're
40 # not on Linux, in which case files don't have any linux attrs anyway, so
41 # lacking the functions isn't a problem.
42 get_linux_file_attr = set_linux_file_attr = None
45 # WARNING: the metadata encoding is *not* stable yet. Caveat emptor!
47 # Q: Consider hardlink support?
48 # Q: Is it OK to store raw linux attr (chattr) flags?
49 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
50 # Q: Is the application of posix1e has_extended() correct?
51 # Q: Is one global --numeric-ids argument sufficient?
52 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
53 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
55 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
56 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
57 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
58 # FIXME: Consider pack('vvvvsss', ...) optimization.
59 # FIXME: Consider caching users/groups.
63 # osx (varies between hfs and hfs+):
64 # type - regular dir char block fifo socket ...
65 # perms - rwxrwxrwxsgt
66 # times - ctime atime mtime
69 # hard-link-info (hfs+ only)
72 # attributes-osx see chflags
78 # type - regular dir ...
79 # times - creation, modification, posix change, access
82 # attributes - see attrib
84 # forks (alternate data streams)
88 # type - regular dir ...
89 # perms - rwxrwxrwx (maybe - see wikipedia)
90 # times - creation, modification, access
91 # attributes - see attrib
95 _have_lchmod = hasattr(os, 'lchmod')
98 def _clean_up_path_for_archive(p):
99 # Not the most efficient approach.
102 # Take everything after any '/../'.
103 pos = result.rfind('/../')
105 result = result[result.rfind('/../') + 4:]
107 # Take everything after any remaining '../'.
108 if result.startswith("../"):
111 # Remove any '/./' sequences.
112 pos = result.find('/./')
114 result = result[0:pos] + '/' + result[pos + 3:]
115 pos = result.find('/./')
117 # Remove any leading '/'s.
118 result = result.lstrip('/')
120 # Replace '//' with '/' everywhere.
121 pos = result.find('//')
123 result = result[0:pos] + '/' + result[pos + 2:]
124 pos = result.find('//')
126 # Take everything after any remaining './'.
127 if result.startswith('./'):
130 # Take everything before any remaining '/.'.
131 if result.endswith('/.'):
134 if result == '' or result.endswith('/..'):
141 if p.startswith('/'):
143 if p.find('/../') != -1:
145 if p.startswith('../'):
147 if p.endswith('/..'):
152 def _clean_up_extract_path(p):
153 result = p.lstrip('/')
156 elif _risky_path(result):
162 # These tags are currently conceptually private to Metadata, and they
163 # must be unique, and must *never* be changed.
166 _rec_tag_common = 2 # times, user, group, type, perms, etc. (legacy/broken)
167 _rec_tag_symlink_target = 3
168 _rec_tag_posix1e_acl = 4 # getfacl(1), setfacl(1), etc.
169 _rec_tag_nfsv4_acl = 5 # intended to supplant posix1e? (unimplemented)
170 _rec_tag_linux_attr = 6 # lsattr(1) chattr(1)
171 _rec_tag_linux_xattr = 7 # getfattr(1) setfattr(1)
172 _rec_tag_hardlink_target = 8 # hard link target path
173 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
176 class ApplyError(Exception):
177 # Thrown when unable to apply any given bit of metadata to a path.
182 # Metadata is stored as a sequence of tagged binary records. Each
183 # record will have some subset of add, encode, load, create, and
184 # apply methods, i.e. _add_foo...
186 # We do allow an "empty" object as a special case, i.e. no
187 # records. One can be created by trying to write Metadata(), and
188 # for such an object, read() will return None. This is used by
189 # "bup save", for example, as a placeholder in cases where
192 # NOTE: if any relevant fields are added or removed, be sure to
193 # update same_file() below.
197 # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
198 # must be non-negative and < 10**9.
200 def _add_common(self, path, st):
203 self.atime = st.st_atime
204 self.mtime = st.st_mtime
205 self.ctime = st.st_ctime
206 self.user = self.group = ''
207 entry = pwd_from_uid(st.st_uid)
209 self.user = entry.pw_name
210 entry = grp_from_gid(st.st_gid)
212 self.group = entry.gr_name
213 self.mode = st.st_mode
214 # Only collect st_rdev if we might need it for a mknod()
215 # during restore. On some platforms (i.e. kFreeBSD), it isn't
216 # stable for other file types. For example "cp -a" will
217 # change it for a plain file.
218 if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
219 self.rdev = st.st_rdev
223 def _same_common(self, other):
224 """Return true or false to indicate similarity in the hardlink sense."""
225 return self.uid == other.uid \
226 and self.gid == other.gid \
227 and self.rdev == other.rdev \
228 and self.mtime == other.mtime \
229 and self.ctime == other.ctime \
230 and self.user == other.user \
231 and self.group == other.group
233 def _encode_common(self):
236 atime = xstat.nsecs_to_timespec(self.atime)
237 mtime = xstat.nsecs_to_timespec(self.mtime)
238 ctime = xstat.nsecs_to_timespec(self.ctime)
239 result = vint.pack('vvsvsvvVvVvV',
254 def _load_common_rec(self, port, legacy_format=False):
255 unpack_fmt = 'vvsvsvvVvVvV'
257 unpack_fmt = 'VVsVsVvVvVvV'
258 data = vint.read_bvec(port)
270 ctime_ns) = vint.unpack(unpack_fmt, data)
271 self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
272 self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
273 self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
275 def _recognized_file_type(self):
276 return stat.S_ISREG(self.mode) \
277 or stat.S_ISDIR(self.mode) \
278 or stat.S_ISCHR(self.mode) \
279 or stat.S_ISBLK(self.mode) \
280 or stat.S_ISFIFO(self.mode) \
281 or stat.S_ISSOCK(self.mode) \
282 or stat.S_ISLNK(self.mode)
284 def _create_via_common_rec(self, path, create_symlinks=True):
286 raise ApplyError('no metadata - cannot create path ' + path)
288 # If the path already exists and is a dir, try rmdir.
289 # If the path already exists and is anything else, try unlink.
292 st = xstat.lstat(path)
294 if e.errno != errno.ENOENT:
297 if stat.S_ISDIR(st.st_mode):
301 if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
302 msg = 'refusing to overwrite non-empty dir ' + path
308 if stat.S_ISREG(self.mode):
309 assert(self._recognized_file_type())
310 fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
312 elif stat.S_ISDIR(self.mode):
313 assert(self._recognized_file_type())
315 elif stat.S_ISCHR(self.mode):
316 assert(self._recognized_file_type())
317 os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
318 elif stat.S_ISBLK(self.mode):
319 assert(self._recognized_file_type())
320 os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
321 elif stat.S_ISFIFO(self.mode):
322 assert(self._recognized_file_type())
323 os.mknod(path, 0600 | stat.S_IFIFO)
324 elif stat.S_ISSOCK(self.mode):
326 os.mknod(path, 0600 | stat.S_IFSOCK)
328 if e.errno in (errno.EINVAL, errno.EPERM):
329 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
333 elif stat.S_ISLNK(self.mode):
334 assert(self._recognized_file_type())
335 if self.symlink_target and create_symlinks:
336 # on MacOS, symlink() permissions depend on umask, and there's
337 # no way to chown a symlink after creating it, so we have to
339 oldumask = os.umask((self.mode & 0777) ^ 0777)
341 os.symlink(self.symlink_target, path)
344 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
346 assert(not self._recognized_file_type())
347 add_error('not creating "%s" with unrecognized mode "0x%x"\n'
350 def _apply_common_rec(self, path, restore_numeric_ids=False):
352 raise ApplyError('no metadata - cannot apply to ' + path)
354 # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
355 # EACCES errors at this stage are fatal for the current path.
356 if lutime and stat.S_ISLNK(self.mode):
358 lutime(path, (self.atime, self.mtime))
360 if e.errno == errno.EACCES:
361 raise ApplyError('lutime: %s' % e)
366 utime(path, (self.atime, self.mtime))
368 if e.errno == errno.EACCES:
369 raise ApplyError('utime: %s' % e)
373 # Implement tar/rsync-like semantics; see bup-restore(1).
374 # FIXME: should we consider caching user/group name <-> id
375 # mappings, getgroups(), etc.?
376 uid = gid = -1 # By default, do nothing.
380 if not restore_numeric_ids:
381 if self.uid != 0 and self.user:
382 entry = pwd_from_name(self.user)
385 if self.gid != 0 and self.group:
386 entry = grp_from_name(self.group)
389 else: # not superuser - only consider changing the group/gid
390 user_gids = os.getgroups()
391 if self.gid in user_gids:
393 if not restore_numeric_ids and self.gid != 0:
394 # The grp might not exist on the local system.
395 grps = filter(None, [grp_from_gid(x) for x in user_gids])
396 if self.group in [x.gr_name for x in grps]:
397 g = grp_from_name(self.group)
401 if uid != -1 or gid != -1:
403 os.lchown(path, uid, gid)
405 if e.errno == errno.EPERM:
406 add_error('lchown: %s' % e)
407 elif sys.platform.startswith('cygwin') \
408 and e.errno == errno.EINVAL:
409 add_error('lchown: unknown uid/gid (%d/%d) for %s'
415 os.lchmod(path, stat.S_IMODE(self.mode))
416 elif not stat.S_ISLNK(self.mode):
417 os.chmod(path, stat.S_IMODE(self.mode))
422 def _encode_path(self):
424 return vint.pack('s', self.path)
428 def _load_path_rec(self, port):
429 self.path = vint.unpack('s', vint.read_bvec(port))[0]
434 def _add_symlink_target(self, path, st):
436 if stat.S_ISLNK(st.st_mode):
437 self.symlink_target = os.readlink(path)
439 add_error('readlink: %s', e)
441 def _encode_symlink_target(self):
442 return self.symlink_target
444 def _load_symlink_target_rec(self, port):
445 self.symlink_target = vint.read_bvec(port)
450 def _add_hardlink_target(self, target):
451 self.hardlink_target = target
453 def _same_hardlink_target(self, other):
454 """Return true or false to indicate similarity in the hardlink sense."""
455 return self.hardlink_target == other.hardlink_target
457 def _encode_hardlink_target(self):
458 return self.hardlink_target
460 def _load_hardlink_target_rec(self, port):
461 self.hardlink_target = vint.read_bvec(port)
464 ## POSIX1e ACL records
466 # Recorded as a list:
467 # [txt_id_acl, num_id_acl]
468 # or, if a directory:
469 # [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
470 # The numeric/text distinction only matters when reading/restoring
472 def _add_posix1e_acl(self, path, st):
473 if not posix1e: return
474 if not stat.S_ISLNK(st.st_mode):
478 if posix1e.has_extended(path):
479 acl = posix1e.ACL(file=path)
480 acls = [acl, acl] # txt and num are the same
481 if stat.S_ISDIR(st.st_mode):
482 def_acl = posix1e.ACL(filedef=path)
483 def_acls = [def_acl, def_acl]
484 except EnvironmentError, e:
485 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
488 txt_flags = posix1e.TEXT_ABBREVIATE
489 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
490 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
491 acls[1].to_any_text('', '\n', num_flags)]
493 acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
494 acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
495 self.posix1e_acl = acl_rep
497 def _same_posix1e_acl(self, other):
498 """Return true or false to indicate similarity in the hardlink sense."""
499 return self.posix1e_acl == other.posix1e_acl
501 def _encode_posix1e_acl(self):
502 # Encode as two strings (w/default ACL string possibly empty).
504 acls = self.posix1e_acl
506 acls.extend(['', ''])
507 return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
511 def _load_posix1e_acl_rec(self, port):
512 acl_rep = vint.unpack('ssss', vint.read_bvec(port))
514 acl_rep = acl_rep[:2]
515 self.posix1e_acl = acl_rep
517 def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
518 def apply_acl(acl_rep, kind):
520 acl = posix1e.ACL(text = acl_rep)
523 # pylibacl appears to return an IOError with errno
524 # set to 0 if a group referred to by the ACL rep
525 # doesn't exist on the current system.
526 raise ApplyError("POSIX1e ACL: can't create %r for %r"
531 acl.applyto(path, kind)
533 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
534 raise ApplyError('POSIX1e ACL applyto: %s' % e)
540 add_error("%s: can't restore ACLs; posix1e support missing.\n"
544 acls = self.posix1e_acl
546 if restore_numeric_ids:
547 apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
549 apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
550 if restore_numeric_ids:
551 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
553 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
556 ## Linux attributes (lsattr(1), chattr(1))
558 def _add_linux_attr(self, path, st):
559 if not get_linux_file_attr: return
560 if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
562 attr = get_linux_file_attr(path)
564 self.linux_attr = attr
566 if e.errno == errno.EACCES:
567 add_error('read Linux attr: %s' % e)
568 elif e.errno in (errno.ENOTTY, errno.ENOSYS, errno.EOPNOTSUPP):
569 # Assume filesystem doesn't support attrs.
574 def _same_linux_attr(self, other):
575 """Return true or false to indicate similarity in the hardlink sense."""
576 return self.linux_attr == other.linux_attr
578 def _encode_linux_attr(self):
580 return vint.pack('V', self.linux_attr)
584 def _load_linux_attr_rec(self, port):
585 data = vint.read_bvec(port)
586 self.linux_attr = vint.unpack('V', data)[0]
588 def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
590 if not set_linux_file_attr:
591 add_error("%s: can't restore linuxattrs: "
592 "linuxattr support missing.\n" % path)
595 set_linux_file_attr(path, self.linux_attr)
597 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS,
599 raise ApplyError('Linux chattr: %s (0x%s)'
600 % (e, hex(self.linux_attr)))
605 ## Linux extended attributes (getfattr(1), setfattr(1))
607 def _add_linux_xattr(self, path, st):
610 self.linux_xattr = xattr.get_all(path, nofollow=True)
611 except EnvironmentError, e:
612 if e.errno != errno.EOPNOTSUPP:
615 def _same_linux_xattr(self, other):
616 """Return true or false to indicate similarity in the hardlink sense."""
617 return self.linux_xattr == other.linux_xattr
619 def _encode_linux_xattr(self):
621 result = vint.pack('V', len(self.linux_xattr))
622 for name, value in self.linux_xattr:
623 result += vint.pack('ss', name, value)
628 def _load_linux_xattr_rec(self, file):
629 data = vint.read_bvec(file)
630 memfile = StringIO(data)
632 for i in range(vint.read_vuint(memfile)):
633 key = vint.read_bvec(memfile)
634 value = vint.read_bvec(memfile)
635 result.append((key, value))
636 self.linux_xattr = result
638 def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
641 add_error("%s: can't restore xattr; xattr support missing.\n"
644 if not self.linux_xattr:
647 existing_xattrs = set(xattr.list(path, nofollow=True))
649 if e.errno == errno.EACCES:
650 raise ApplyError('xattr.set: %s' % e)
653 for k, v in self.linux_xattr:
654 if k not in existing_xattrs \
655 or v != xattr.get(path, k, nofollow=True):
657 xattr.set(path, k, v, nofollow=True)
659 if e.errno == errno.EPERM \
660 or e.errno == errno.EOPNOTSUPP:
661 raise ApplyError('xattr.set: %s' % e)
664 existing_xattrs -= frozenset([k])
665 for k in existing_xattrs:
667 xattr.remove(path, k, nofollow=True)
669 if e.errno == errno.EPERM:
670 raise ApplyError('xattr.remove: %s' % e)
679 self.symlink_target = None
680 self.hardlink_target = None
681 self.linux_attr = None
682 self.linux_xattr = None
683 self.posix1e_acl = None
685 def write(self, port, include_path=True):
686 records = include_path and [(_rec_tag_path, self._encode_path())] or []
687 records.extend([(_rec_tag_common_v2, self._encode_common()),
688 (_rec_tag_symlink_target,
689 self._encode_symlink_target()),
690 (_rec_tag_hardlink_target,
691 self._encode_hardlink_target()),
692 (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
693 (_rec_tag_linux_attr, self._encode_linux_attr()),
694 (_rec_tag_linux_xattr, self._encode_linux_xattr())])
695 for tag, data in records:
697 vint.write_vuint(port, tag)
698 vint.write_bvec(port, data)
699 vint.write_vuint(port, _rec_tag_end)
701 def encode(self, include_path=True):
703 self.write(port, include_path)
704 return port.getvalue()
708 # This method should either return a valid Metadata object,
709 # return None if there was no information at all (just a
710 # _rec_tag_end), throw EOFError if there was nothing at all to
711 # read, or throw an Exception if a valid object could not be
713 tag = vint.read_vuint(port)
714 if tag == _rec_tag_end:
716 try: # From here on, EOF is an error.
718 while True: # only exit is error (exception) or _rec_tag_end
719 if tag == _rec_tag_path:
720 result._load_path_rec(port)
721 elif tag == _rec_tag_common_v2:
722 result._load_common_rec(port)
723 elif tag == _rec_tag_symlink_target:
724 result._load_symlink_target_rec(port)
725 elif tag == _rec_tag_hardlink_target:
726 result._load_hardlink_target_rec(port)
727 elif tag == _rec_tag_posix1e_acl:
728 result._load_posix1e_acl_rec(port)
729 elif tag == _rec_tag_linux_attr:
730 result._load_linux_attr_rec(port)
731 elif tag == _rec_tag_linux_xattr:
732 result._load_linux_xattr_rec(port)
733 elif tag == _rec_tag_end:
735 elif tag == _rec_tag_common: # Should be very rare.
736 result._load_common_rec(port, legacy_format = True)
737 else: # unknown record
739 tag = vint.read_vuint(port)
741 raise Exception("EOF while reading Metadata")
744 return stat.S_ISDIR(self.mode)
746 def create_path(self, path, create_symlinks=True):
747 self._create_via_common_rec(path, create_symlinks=create_symlinks)
749 def apply_to_path(self, path=None, restore_numeric_ids=False):
750 # apply metadata to path -- file must exist
754 raise Exception('Metadata.apply_to_path() called with no path')
755 if not self._recognized_file_type():
756 add_error('not applying metadata to "%s"' % path
757 + ' with unrecognized mode "0x%x"\n' % self.mode)
759 num_ids = restore_numeric_ids
760 for apply_metadata in (self._apply_common_rec,
761 self._apply_posix1e_acl_rec,
762 self._apply_linux_attr_rec,
763 self._apply_linux_xattr_rec):
765 apply_metadata(path, restore_numeric_ids=num_ids)
766 except ApplyError, e:
769 def same_file(self, other):
770 """Compare this to other for equivalency. Return true if
771 their information implies they could represent the same file
772 on disk, in the hardlink sense. Assume they're both regular
774 return self._same_common(other) \
775 and self._same_hardlink_target(other) \
776 and self._same_posix1e_acl(other) \
777 and self._same_linux_attr(other) \
778 and self._same_linux_xattr(other)
781 def from_path(path, statinfo=None, archive_path=None,
782 save_symlinks=True, hardlink_target=None):
784 result.path = archive_path
785 st = statinfo or xstat.lstat(path)
786 result.size = st.st_size
787 result._add_common(path, st)
789 result._add_symlink_target(path, st)
790 result._add_hardlink_target(hardlink_target)
791 result._add_posix1e_acl(path, st)
792 result._add_linux_attr(path, st)
793 result._add_linux_xattr(path, st)
797 def save_tree(output_file, paths,
803 # Issue top-level rewrite warnings.
805 safe_path = _clean_up_path_for_archive(path)
806 if safe_path != path:
807 log('archiving "%s" as "%s"\n' % (path, safe_path))
811 safe_path = _clean_up_path_for_archive(p)
813 if stat.S_ISDIR(st.st_mode):
815 m = from_path(p, statinfo=st, archive_path=safe_path,
816 save_symlinks=save_symlinks)
818 print >> sys.stderr, m.path
819 m.write(output_file, include_path=write_paths)
821 start_dir = os.getcwd()
823 for (p, st) in recursive_dirlist(paths, xdev=xdev):
824 dirlist_dir = os.getcwd()
826 safe_path = _clean_up_path_for_archive(p)
827 m = from_path(p, statinfo=st, archive_path=safe_path,
828 save_symlinks=save_symlinks)
830 print >> sys.stderr, m.path
831 m.write(output_file, include_path=write_paths)
832 os.chdir(dirlist_dir)
837 def _set_up_path(meta, create_symlinks=True):
838 # Allow directories to exist as a special case -- might have
839 # been created by an earlier longer path.
843 parent = os.path.dirname(meta.path)
846 meta.create_path(meta.path, create_symlinks=create_symlinks)
849 all_fields = frozenset(['path',
866 def summary_str(meta):
867 mode_val = xstat.mode_str(meta.mode)
870 user_val = str(meta.uid)
871 group_val = meta.group
873 group_val = str(meta.gid)
874 size_or_dev_val = '-'
875 if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
876 size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
878 size_or_dev_val = meta.size
879 mtime_secs = xstat.fstime_floor_secs(meta.mtime)
880 time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
881 path_val = meta.path or ''
882 if stat.S_ISLNK(meta.mode):
883 path_val += ' -> ' + meta.symlink_target
884 return '%-10s %-11s %11s %16s %s' % (mode_val,
885 user_val + "/" + group_val,
891 def detailed_str(meta, fields = None):
892 # FIXME: should optional fields be omitted, or empty i.e. "rdev:
893 # 0", "link-target:", etc.
899 path = meta.path or ''
900 result.append('path: ' + path)
902 result.append('mode: %s (%s)' % (oct(meta.mode),
903 xstat.mode_str(meta.mode)))
904 if 'link-target' in fields and stat.S_ISLNK(meta.mode):
905 result.append('link-target: ' + meta.symlink_target)
908 result.append('rdev: %d,%d' % (os.major(meta.rdev),
909 os.minor(meta.rdev)))
911 result.append('rdev: 0')
912 if 'size' in fields and meta.size:
913 result.append('size: ' + str(meta.size))
915 result.append('uid: ' + str(meta.uid))
917 result.append('gid: ' + str(meta.gid))
919 result.append('user: ' + meta.user)
920 if 'group' in fields:
921 result.append('group: ' + meta.group)
922 if 'atime' in fields:
923 # If we don't have xstat.lutime, that means we have to use
924 # utime(), and utime() has no way to set the mtime/atime of a
925 # symlink. Thus, the mtime/atime of a symlink is meaningless,
926 # so let's not report it. (That way scripts comparing
927 # before/after won't trigger.)
928 if xstat.lutime or not stat.S_ISLNK(meta.mode):
929 result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
931 result.append('atime: 0')
932 if 'mtime' in fields:
933 if xstat.lutime or not stat.S_ISLNK(meta.mode):
934 result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
936 result.append('mtime: 0')
937 if 'ctime' in fields:
938 result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
939 if 'linux-attr' in fields and meta.linux_attr:
940 result.append('linux-attr: ' + hex(meta.linux_attr))
941 if 'linux-xattr' in fields and meta.linux_xattr:
942 for name, value in meta.linux_xattr:
943 result.append('linux-xattr: %s -> %s' % (name, repr(value)))
944 if 'posix1e-acl' in fields and meta.posix1e_acl:
945 acl = meta.posix1e_acl[0]
946 result.append('posix1e-acl: ' + acl + '\n')
947 if stat.S_ISDIR(meta.mode):
948 def_acl = meta.posix1e_acl[2]
949 result.append('posix1e-acl-default: ' + def_acl + '\n')
950 return '\n'.join(result)
953 class _ArchiveIterator:
956 return Metadata.read(self._file)
958 raise StopIteration()
963 def __init__(self, file):
967 def display_archive(file):
970 for meta in _ArchiveIterator(file):
973 print detailed_str(meta)
976 for meta in _ArchiveIterator(file):
977 print summary_str(meta)
979 for meta in _ArchiveIterator(file):
981 print >> sys.stderr, \
982 'bup: no metadata path, but asked to only display path', \
983 '(increase verbosity?)'
988 def start_extract(file, create_symlinks=True):
989 for meta in _ArchiveIterator(file):
990 if not meta: # Hit end record.
993 print >> sys.stderr, meta.path
994 xpath = _clean_up_extract_path(meta.path)
996 add_error(Exception('skipping risky path "%s"' % meta.path))
999 _set_up_path(meta, create_symlinks=create_symlinks)
1002 def finish_extract(file, restore_numeric_ids=False):
1004 for meta in _ArchiveIterator(file):
1005 if not meta: # Hit end record.
1007 xpath = _clean_up_extract_path(meta.path)
1009 add_error(Exception('skipping risky path "%s"' % dir.path))
1011 if os.path.isdir(meta.path):
1012 all_dirs.append(meta)
1015 print >> sys.stderr, meta.path
1016 meta.apply_to_path(path=xpath,
1017 restore_numeric_ids=restore_numeric_ids)
1018 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1019 for dir in all_dirs:
1020 # Don't need to check xpath -- won't be in all_dirs if not OK.
1021 xpath = _clean_up_extract_path(dir.path)
1023 print >> sys.stderr, dir.path
1024 dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1027 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1028 # For now, just store all the directories and handle them last,
1031 for meta in _ArchiveIterator(file):
1032 if not meta: # Hit end record.
1034 xpath = _clean_up_extract_path(meta.path)
1036 add_error(Exception('skipping risky path "%s"' % meta.path))
1040 print >> sys.stderr, '+', meta.path
1041 _set_up_path(meta, create_symlinks=create_symlinks)
1042 if os.path.isdir(meta.path):
1043 all_dirs.append(meta)
1046 print >> sys.stderr, '=', meta.path
1047 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1048 all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1049 for dir in all_dirs:
1050 # Don't need to check xpath -- won't be in all_dirs if not OK.
1051 xpath = _clean_up_extract_path(dir.path)
1053 print >> sys.stderr, '=', xpath
1054 # Shouldn't have to check for risky paths here (omitted above).
1055 dir.apply_to_path(path=dir.path,
1056 restore_numeric_ids=restore_numeric_ids)