]> arthur.barton.de Git - bup.git/blob - lib/bup/metadata.py
Completely disable ACL support on OS X for now.
[bup.git] / lib / bup / metadata.py
1 """Metadata read/write support for bup."""
2
3 # Copyright (C) 2010 Rob Browning
4 #
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
14
15 xattr = None
16 if sys.platform.startswith('linux'):
17     try:
18         import xattr
19     except ImportError:
20         log('Warning: Linux xattr support missing; install python-pyxattr.\n')
21     if xattr:
22         try:
23             xattr.get_all
24         except AttributeError:
25             log('Warning: python-xattr module is too old; '
26                 'install python-pyxattr instead.\n')
27             xattr = None
28
29 posix1e = None
30 if not (sys.platform.startswith('cygwin') or sys.platform.startswith('darwin')):
31     try:
32         import posix1e
33     except ImportError:
34         log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
35
36 try:
37     from bup._helpers import get_linux_file_attr, set_linux_file_attr
38 except ImportError:
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
43     
44
45 # WARNING: the metadata encoding is *not* stable yet.  Caveat emptor!
46
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*?
54
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.
60
61 ## FS notes:
62 #
63 # osx (varies between hfs and hfs+):
64 #   type - regular dir char block fifo socket ...
65 #   perms - rwxrwxrwxsgt
66 #   times - ctime atime mtime
67 #   uid
68 #   gid
69 #   hard-link-info (hfs+ only)
70 #   link-target
71 #   device-major/minor
72 #   attributes-osx see chflags
73 #   content-type
74 #   content-creator
75 #   forks
76 #
77 # ntfs
78 #   type - regular dir ...
79 #   times - creation, modification, posix change, access
80 #   hard-link-info
81 #   link-target
82 #   attributes - see attrib
83 #   ACLs
84 #   forks (alternate data streams)
85 #   crtime?
86 #
87 # fat
88 #   type - regular dir ...
89 #   perms - rwxrwxrwx (maybe - see wikipedia)
90 #   times - creation, modification, access
91 #   attributes - see attrib
92
93 verbose = 0
94
95 _have_lchmod = hasattr(os, 'lchmod')
96
97
98 def _clean_up_path_for_archive(p):
99     # Not the most efficient approach.
100     result = p
101
102     # Take everything after any '/../'.
103     pos = result.rfind('/../')
104     if pos != -1:
105         result = result[result.rfind('/../') + 4:]
106
107     # Take everything after any remaining '../'.
108     if result.startswith("../"):
109         result = result[3:]
110
111     # Remove any '/./' sequences.
112     pos = result.find('/./')
113     while pos != -1:
114         result = result[0:pos] + '/' + result[pos + 3:]
115         pos = result.find('/./')
116
117     # Remove any leading '/'s.
118     result = result.lstrip('/')
119
120     # Replace '//' with '/' everywhere.
121     pos = result.find('//')
122     while pos != -1:
123         result = result[0:pos] + '/' + result[pos + 2:]
124         pos = result.find('//')
125
126     # Take everything after any remaining './'.
127     if result.startswith('./'):
128         result = result[2:]
129
130     # Take everything before any remaining '/.'.
131     if result.endswith('/.'):
132         result = result[:-2]
133
134     if result == '' or result.endswith('/..'):
135         result = '.'
136
137     return result
138
139
140 def _risky_path(p):
141     if p.startswith('/'):
142         return True
143     if p.find('/../') != -1:
144         return True
145     if p.startswith('../'):
146         return True
147     if p.endswith('/..'):
148         return True
149     return False
150
151
152 def _clean_up_extract_path(p):
153     result = p.lstrip('/')
154     if result == '':
155         return '.'
156     elif _risky_path(result):
157         return None
158     else:
159         return result
160
161
162 # These tags are currently conceptually private to Metadata, and they
163 # must be unique, and must *never* be changed.
164 _rec_tag_end = 0
165 _rec_tag_path = 1
166 _rec_tag_common = 2           # times, user, group, type, perms, etc.
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 acls?
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
174
175 class ApplyError(Exception):
176     # Thrown when unable to apply any given bit of metadata to a path.
177     pass
178
179
180 class Metadata:
181     # Metadata is stored as a sequence of tagged binary records.  Each
182     # record will have some subset of add, encode, load, create, and
183     # apply methods, i.e. _add_foo...
184
185     # We do allow an "empty" object as a special case, i.e. no
186     # records.  One can be created by trying to write Metadata(), and
187     # for such an object, read() will return None.  This is used by
188     # "bup save", for example, as a placeholder in cases where
189     # from_path() fails.
190
191     # NOTE: if any relevant fields are added or removed, be sure to
192     # update same_file() below.
193
194     ## Common records
195
196     # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
197     # must be non-negative and < 10**9.
198
199     def _add_common(self, path, st):
200         self.uid = st.st_uid
201         self.gid = st.st_gid
202         self.rdev = st.st_rdev
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)
208         if entry:
209             self.user = entry.pw_name
210         entry = grp_from_gid(st.st_gid)
211         if entry:
212             self.group = entry.gr_name
213         self.mode = st.st_mode
214
215     def _same_common(self, other):
216         """Return true or false to indicate similarity in the hardlink sense."""
217         return self.uid == other.uid \
218             and self.gid == other.gid \
219             and self.rdev == other.rdev \
220             and self.atime == other.atime \
221             and self.mtime == other.mtime \
222             and self.ctime == other.ctime \
223             and self.user == other.user \
224             and self.group == other.group
225
226     def _encode_common(self):
227         if not self.mode:
228             return None
229         atime = xstat.nsecs_to_timespec(self.atime)
230         mtime = xstat.nsecs_to_timespec(self.mtime)
231         ctime = xstat.nsecs_to_timespec(self.ctime)
232         result = vint.pack('VVsVsVvVvVvV',
233                            self.mode,
234                            self.uid,
235                            self.user,
236                            self.gid,
237                            self.group,
238                            self.rdev,
239                            atime[0],
240                            atime[1],
241                            mtime[0],
242                            mtime[1],
243                            ctime[0],
244                            ctime[1])
245         return result
246
247     def _load_common_rec(self, port):
248         data = vint.read_bvec(port)
249         (self.mode,
250          self.uid,
251          self.user,
252          self.gid,
253          self.group,
254          self.rdev,
255          self.atime,
256          atime_ns,
257          self.mtime,
258          mtime_ns,
259          self.ctime,
260          ctime_ns) = vint.unpack('VVsVsVvVvVvV', data)
261         self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
262         self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
263         self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
264
265     def _recognized_file_type(self):
266         return stat.S_ISREG(self.mode) \
267             or stat.S_ISDIR(self.mode) \
268             or stat.S_ISCHR(self.mode) \
269             or stat.S_ISBLK(self.mode) \
270             or stat.S_ISFIFO(self.mode) \
271             or stat.S_ISSOCK(self.mode) \
272             or stat.S_ISLNK(self.mode)
273
274     def _create_via_common_rec(self, path, create_symlinks=True):
275         if not self.mode:
276             raise ApplyError('no metadata - cannot create path ' + path)
277
278         # If the path already exists and is a dir, try rmdir.
279         # If the path already exists and is anything else, try unlink.
280         st = None
281         try:
282             st = xstat.lstat(path)
283         except OSError, e:
284             if e.errno != errno.ENOENT:
285                 raise
286         if st:
287             if stat.S_ISDIR(st.st_mode):
288                 try:
289                     os.rmdir(path)
290                 except OSError, e:
291                     if e.errno == errno.ENOTEMPTY:
292                         msg = 'refusing to overwrite non-empty dir ' + path
293                         raise Exception(msg)
294                     raise
295             else:
296                 os.unlink(path)
297
298         if stat.S_ISREG(self.mode):
299             assert(self._recognized_file_type())
300             fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
301             os.close(fd)
302         elif stat.S_ISDIR(self.mode):
303             assert(self._recognized_file_type())
304             os.mkdir(path, 0700)
305         elif stat.S_ISCHR(self.mode):
306             assert(self._recognized_file_type())
307             os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
308         elif stat.S_ISBLK(self.mode):
309             assert(self._recognized_file_type())
310             os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
311         elif stat.S_ISFIFO(self.mode):
312             assert(self._recognized_file_type())
313             os.mknod(path, 0600 | stat.S_IFIFO)
314         elif stat.S_ISSOCK(self.mode):
315             if not sys.platform.startswith('cygwin'):
316                 os.mknod(path, 0600 | stat.S_IFSOCK)
317             else:
318                 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
319                 s.bind(path)
320         elif stat.S_ISLNK(self.mode):
321             assert(self._recognized_file_type())
322             if self.symlink_target and create_symlinks:
323                 # on MacOS, symlink() permissions depend on umask, and there's
324                 # no way to chown a symlink after creating it, so we have to
325                 # be careful here!
326                 oldumask = os.umask((self.mode & 0777) ^ 0777)
327                 try:
328                     os.symlink(self.symlink_target, path)
329                 finally:
330                     os.umask(oldumask)
331         # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
332         else:
333             assert(not self._recognized_file_type())
334             add_error('not creating "%s" with unrecognized mode "0x%x"\n'
335                       % (path, self.mode))
336
337     def _apply_common_rec(self, path, restore_numeric_ids=False):
338         if not self.mode:
339             raise ApplyError('no metadata - cannot apply to ' + path)
340
341         # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
342         # EACCES errors at this stage are fatal for the current path.
343         if lutime and stat.S_ISLNK(self.mode):
344             try:
345                 lutime(path, (self.atime, self.mtime))
346             except OSError, e:
347                 if e.errno == errno.EACCES:
348                     raise ApplyError('lutime: %s' % e)
349                 else:
350                     raise
351         else:
352             try:
353                 utime(path, (self.atime, self.mtime))
354             except OSError, e:
355                 if e.errno == errno.EACCES:
356                     raise ApplyError('utime: %s' % e)
357                 else:
358                     raise
359
360         # Implement tar/rsync-like semantics; see bup-restore(1).
361         # FIXME: should we consider caching user/group name <-> id
362         # mappings, getgroups(), etc.?
363         uid = gid = -1 # By default, do nothing.
364         if is_superuser():
365             uid = self.uid
366             gid = self.gid
367             if not restore_numeric_ids:
368                 if self.uid != 0 and self.user:
369                     entry = pwd_from_name(self.user)
370                     if entry:
371                         uid = entry.pw_uid
372                 if self.gid != 0 and self.group:
373                     entry = grp_from_name(self.group)
374                     if entry:
375                         gid = entry.gr_gid
376         else: # not superuser - only consider changing the group/gid
377             user_gids = os.getgroups()
378             if self.gid in user_gids:
379                 gid = self.gid
380             if not restore_numeric_ids and self.gid != 0:
381                 # The grp might not exist on the local system.
382                 grps = filter(None, [grp_from_gid(x) for x in user_gids])
383                 if self.group in [x.gr_name for x in grps]:
384                     g = grp_from_name(self.group)
385                     if g:
386                         gid = g.gr_gid
387
388         if uid != -1 or gid != -1:
389             try:
390                 os.lchown(path, uid, gid)
391             except OSError, e:
392                 if e.errno == errno.EPERM:
393                     add_error('lchown: %s' %  e)
394                 else:
395                     raise
396
397         if _have_lchmod:
398             os.lchmod(path, stat.S_IMODE(self.mode))
399         elif not stat.S_ISLNK(self.mode):
400             os.chmod(path, stat.S_IMODE(self.mode))
401
402
403     ## Path records
404
405     def _encode_path(self):
406         if self.path:
407             return vint.pack('s', self.path)
408         else:
409             return None
410
411     def _load_path_rec(self, port):
412         self.path = vint.unpack('s', vint.read_bvec(port))[0]
413
414
415     ## Symlink targets
416
417     def _add_symlink_target(self, path, st):
418         try:
419             if stat.S_ISLNK(st.st_mode):
420                 self.symlink_target = os.readlink(path)
421         except OSError, e:
422             add_error('readlink: %s', e)
423
424     def _encode_symlink_target(self):
425         return self.symlink_target
426
427     def _load_symlink_target_rec(self, port):
428         self.symlink_target = vint.read_bvec(port)
429
430
431     ## Hardlink targets
432
433     def _add_hardlink_target(self, target):
434         self.hardlink_target = target
435
436     def _same_hardlink_target(self, other):
437         """Return true or false to indicate similarity in the hardlink sense."""
438         return self.hardlink_target == other.hardlink_target
439
440     def _encode_hardlink_target(self):
441         return self.hardlink_target
442
443     def _load_hardlink_target_rec(self, port):
444         self.hardlink_target = vint.read_bvec(port)
445
446
447     ## POSIX1e ACL records
448
449     # Recorded as a list:
450     #   [txt_id_acl, num_id_acl]
451     # or, if a directory:
452     #   [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
453     # The numeric/text distinction only matters when reading/restoring
454     # a stored record.
455     def _add_posix1e_acl(self, path, st):
456         if not posix1e: return
457         if not stat.S_ISLNK(st.st_mode):
458             try:
459                 if posix1e.has_extended(path):
460                     acl = posix1e.ACL(file=path)
461                     self.posix1e_acl = [acl, acl] # txt and num are the same
462                     if stat.S_ISDIR(st.st_mode):
463                         acl = posix1e.ACL(filedef=path)
464                         self.posix1e_acl.extend([acl, acl])
465             except EnvironmentError, e:
466                 if e.errno != errno.EOPNOTSUPP:
467                     raise
468
469     def _same_posix1e_acl(self, other):
470         """Return true or false to indicate similarity in the hardlink sense."""
471         return self.posix1e_acl == other.posix1e_acl
472
473     def _encode_posix1e_acl(self):
474         # Encode as two strings (w/default ACL string possibly empty).
475         if self.posix1e_acl:
476             acls = self.posix1e_acl
477             txt_flags = posix1e.TEXT_ABBREVIATE
478             num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
479             acl_reps = [acls[0].to_any_text('', '\n', txt_flags),
480                         acls[1].to_any_text('', '\n', num_flags)]
481             if len(acls) < 3:
482                 acl_reps += ['', '']
483             else:
484                 acl_reps.append(acls[2].to_any_text('', '\n', txt_flags))
485                 acl_reps.append(acls[3].to_any_text('', '\n', num_flags))
486             return vint.pack('ssss',
487                              acl_reps[0], acl_reps[1], acl_reps[2], acl_reps[3])
488         else:
489             return None
490
491     def _load_posix1e_acl_rec(self, port):
492         data = vint.read_bvec(port)
493         acl_reps = vint.unpack('ssss', data)
494         if acl_reps[2] == '':
495             acl_reps = acl_reps[:2]
496         self.posix1e_acl = [posix1e.ACL(text=x) for x in acl_reps]
497
498     def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
499         def apply_acl(acl, kind):
500             try:
501                 acl.applyto(path, kind)
502             except IOError, e:
503                 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
504                     raise ApplyError('POSIX1e ACL applyto: %s' % e)
505                 else:
506                     raise
507
508         if not posix1e:
509             if self.posix1e_acl:
510                 add_error("%s: can't restore ACLs; posix1e support missing.\n"
511                           % path)
512             return
513         if self.posix1e_acl:
514             acls = self.posix1e_acl
515             if len(acls) > 2:
516                 if restore_numeric_ids:
517                     apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
518                 else:
519                     apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
520             if restore_numeric_ids:
521                 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
522             else:
523                 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
524
525
526     ## Linux attributes (lsattr(1), chattr(1))
527
528     def _add_linux_attr(self, path, st):
529         if not get_linux_file_attr: return
530         if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
531             try:
532                 attr = get_linux_file_attr(path)
533                 if attr != 0:
534                     self.linux_attr = attr
535             except OSError, e:
536                 if e.errno == errno.EACCES:
537                     add_error('read Linux attr: %s' % e)
538                 elif e.errno == errno.ENOTTY or e.errno == errno.ENOSYS:
539                     # ENOTTY: Function not implemented.
540                     # ENOSYS: Inappropriate ioctl for device.
541                     # Assume filesystem doesn't support attrs.
542                     return
543                 else:
544                     raise
545
546     def _same_linux_attr(self, other):
547         """Return true or false to indicate similarity in the hardlink sense."""
548         return self.linux_attr == other.linux_attr
549
550     def _encode_linux_attr(self):
551         if self.linux_attr:
552             return vint.pack('V', self.linux_attr)
553         else:
554             return None
555
556     def _load_linux_attr_rec(self, port):
557         data = vint.read_bvec(port)
558         self.linux_attr = vint.unpack('V', data)[0]
559
560     def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
561         if self.linux_attr:
562             if not set_linux_file_attr:
563                 add_error("%s: can't restore linuxattrs: "
564                           "linuxattr support missing.\n" % path)
565                 return
566             try:
567                 set_linux_file_attr(path, self.linux_attr)
568             except OSError, e:
569                 if e.errno == errno.ENOTTY or e.errno == errno.EOPNOTSUPP:
570                     raise ApplyError('Linux chattr: %s' % e)
571                 else:
572                     raise
573
574
575     ## Linux extended attributes (getfattr(1), setfattr(1))
576
577     def _add_linux_xattr(self, path, st):
578         if not xattr: return
579         try:
580             self.linux_xattr = xattr.get_all(path, nofollow=True)
581         except EnvironmentError, e:
582             if e.errno != errno.EOPNOTSUPP:
583                 raise
584
585     def _same_linux_xattr(self, other):
586         """Return true or false to indicate similarity in the hardlink sense."""
587         return self.linux_xattr == other.linux_xattr
588
589     def _encode_linux_xattr(self):
590         if self.linux_xattr:
591             result = vint.pack('V', len(self.linux_xattr))
592             for name, value in self.linux_xattr:
593                 result += vint.pack('ss', name, value)
594             return result
595         else:
596             return None
597
598     def _load_linux_xattr_rec(self, file):
599         data = vint.read_bvec(file)
600         memfile = StringIO(data)
601         result = []
602         for i in range(vint.read_vuint(memfile)):
603             key = vint.read_bvec(memfile)
604             value = vint.read_bvec(memfile)
605             result.append((key, value))
606         self.linux_xattr = result
607
608     def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
609         if not xattr:
610             if self.linux_xattr:
611                 add_error("%s: can't restore xattr; xattr support missing.\n"
612                           % path)
613             return
614         existing_xattrs = set(xattr.list(path, nofollow=True))
615         if self.linux_xattr:
616             for k, v in self.linux_xattr:
617                 if k not in existing_xattrs \
618                         or v != xattr.get(path, k, nofollow=True):
619                     try:
620                         xattr.set(path, k, v, nofollow=True)
621                     except IOError, e:
622                         if e.errno == errno.EPERM \
623                                 or e.errno == errno.EOPNOTSUPP:
624                             raise ApplyError('xattr.set: %s' % e)
625                         else:
626                             raise
627                 existing_xattrs -= frozenset([k])
628             for k in existing_xattrs:
629                 try:
630                     xattr.remove(path, k, nofollow=True)
631                 except IOError, e:
632                     if e.errno == errno.EPERM:
633                         raise ApplyError('xattr.remove: %s' % e)
634                     else:
635                         raise
636
637     def __init__(self):
638         self.mode = None
639         # optional members
640         self.path = None
641         self.size = None
642         self.symlink_target = None
643         self.hardlink_target = None
644         self.linux_attr = None
645         self.linux_xattr = None
646         self.posix1e_acl = None
647         self.posix1e_acl_default = None
648
649     def write(self, port, include_path=True):
650         records = include_path and [(_rec_tag_path, self._encode_path())] or []
651         records.extend([(_rec_tag_common, self._encode_common()),
652                         (_rec_tag_symlink_target,
653                          self._encode_symlink_target()),
654                         (_rec_tag_hardlink_target,
655                          self._encode_hardlink_target()),
656                         (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
657                         (_rec_tag_linux_attr, self._encode_linux_attr()),
658                         (_rec_tag_linux_xattr, self._encode_linux_xattr())])
659         for tag, data in records:
660             if data:
661                 vint.write_vuint(port, tag)
662                 vint.write_bvec(port, data)
663         vint.write_vuint(port, _rec_tag_end)
664
665     def encode(self, include_path=True):
666         port = StringIO()
667         self.write(port, include_path)
668         return port.getvalue()
669
670     @staticmethod
671     def read(port):
672         # This method should either return a valid Metadata object,
673         # return None if there was no information at all (just a
674         # _rec_tag_end), throw EOFError if there was nothing at all to
675         # read, or throw an Exception if a valid object could not be
676         # read completely.
677         tag = vint.read_vuint(port)
678         if tag == _rec_tag_end:
679             return None
680         try: # From here on, EOF is an error.
681             result = Metadata()
682             while True: # only exit is error (exception) or _rec_tag_end
683                 if tag == _rec_tag_path:
684                     result._load_path_rec(port)
685                 elif tag == _rec_tag_common:
686                     result._load_common_rec(port)
687                 elif tag == _rec_tag_symlink_target:
688                     result._load_symlink_target_rec(port)
689                 elif tag == _rec_tag_hardlink_target:
690                     result._load_hardlink_target_rec(port)
691                 elif tag == _rec_tag_posix1e_acl:
692                     result._load_posix1e_acl_rec(port)
693                 elif tag ==_rec_tag_nfsv4_acl:
694                     result._load_nfsv4_acl_rec(port)
695                 elif tag == _rec_tag_linux_attr:
696                     result._load_linux_attr_rec(port)
697                 elif tag == _rec_tag_linux_xattr:
698                     result._load_linux_xattr_rec(port)
699                 elif tag == _rec_tag_end:
700                     return result
701                 else: # unknown record
702                     vint.skip_bvec(port)
703                 tag = vint.read_vuint(port)
704         except EOFError:
705             raise Exception("EOF while reading Metadata")
706
707     def isdir(self):
708         return stat.S_ISDIR(self.mode)
709
710     def create_path(self, path, create_symlinks=True):
711         self._create_via_common_rec(path, create_symlinks=create_symlinks)
712
713     def apply_to_path(self, path=None, restore_numeric_ids=False):
714         # apply metadata to path -- file must exist
715         if not path:
716             path = self.path
717         if not path:
718             raise Exception('Metadata.apply_to_path() called with no path')
719         if not self._recognized_file_type():
720             add_error('not applying metadata to "%s"' % path
721                       + ' with unrecognized mode "0x%x"\n' % self.mode)
722             return
723         num_ids = restore_numeric_ids
724         try:
725             self._apply_common_rec(path, restore_numeric_ids=num_ids)
726             self._apply_posix1e_acl_rec(path, restore_numeric_ids=num_ids)
727             self._apply_linux_attr_rec(path, restore_numeric_ids=num_ids)
728             self._apply_linux_xattr_rec(path, restore_numeric_ids=num_ids)
729         except ApplyError, e:
730             add_error(e)
731
732     def same_file(self, other):
733         """Compare this to other for equivalency.  Return true if
734         their information implies they could represent the same file
735         on disk, in the hardlink sense.  Assume they're both regular
736         files."""
737         return self._same_common(other) \
738             and self._same_hardlink_target(other) \
739             and self._same_posix1e_acl(other) \
740             and self._same_linux_attr(other) \
741             and self._same_linux_xattr(other)
742
743
744 def from_path(path, statinfo=None, archive_path=None,
745               save_symlinks=True, hardlink_target=None):
746     result = Metadata()
747     result.path = archive_path
748     st = statinfo or xstat.lstat(path)
749     result.size = st.st_size
750     result._add_common(path, st)
751     if save_symlinks:
752         result._add_symlink_target(path, st)
753     result._add_hardlink_target(hardlink_target)
754     result._add_posix1e_acl(path, st)
755     result._add_linux_attr(path, st)
756     result._add_linux_xattr(path, st)
757     return result
758
759
760 def save_tree(output_file, paths,
761               recurse=False,
762               write_paths=True,
763               save_symlinks=True,
764               xdev=False):
765
766     # Issue top-level rewrite warnings.
767     for path in paths:
768         safe_path = _clean_up_path_for_archive(path)
769         if safe_path != path:
770             log('archiving "%s" as "%s"\n' % (path, safe_path))
771
772     start_dir = os.getcwd()
773     try:
774         for (p, st) in recursive_dirlist(paths, xdev=xdev):
775             dirlist_dir = os.getcwd()
776             os.chdir(start_dir)
777             safe_path = _clean_up_path_for_archive(p)
778             m = from_path(p, statinfo=st, archive_path=safe_path,
779                           save_symlinks=save_symlinks)
780             if verbose:
781                 print >> sys.stderr, m.path
782             m.write(output_file, include_path=write_paths)
783             os.chdir(dirlist_dir)
784     finally:
785         os.chdir(start_dir)
786
787
788 def _set_up_path(meta, create_symlinks=True):
789     # Allow directories to exist as a special case -- might have
790     # been created by an earlier longer path.
791     if meta.isdir():
792         mkdirp(meta.path)
793     else:
794         parent = os.path.dirname(meta.path)
795         if parent:
796             mkdirp(parent)
797         meta.create_path(meta.path, create_symlinks=create_symlinks)
798
799
800 all_fields = frozenset(['path',
801                         'mode',
802                         'link-target',
803                         'rdev',
804                         'size',
805                         'uid',
806                         'gid',
807                         'user',
808                         'group',
809                         'atime',
810                         'mtime',
811                         'ctime',
812                         'linux-attr',
813                         'linux-xattr',
814                         'posix1e-acl'])
815
816
817 def summary_str(meta):
818     mode_val = xstat.mode_str(meta.mode)
819     user_val = meta.user
820     if not user_val:
821         user_val = str(meta.uid)
822     group_val = meta.group
823     if not group_val:
824         group_val = str(meta.gid)
825     size_or_dev_val = '-'
826     if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
827         size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
828     elif meta.size:
829         size_or_dev_val = meta.size
830     mtime_secs = xstat.fstime_floor_secs(meta.mtime)
831     time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
832     path_val = meta.path or ''
833     if stat.S_ISLNK(meta.mode):
834         path_val += ' -> ' + meta.symlink_target
835     return '%-10s %-11s %11s %16s %s' % (mode_val,
836                                          user_val + "/" + group_val,
837                                          size_or_dev_val,
838                                          time_val,
839                                          path_val)
840
841
842 def detailed_str(meta, fields = None):
843     # FIXME: should optional fields be omitted, or empty i.e. "rdev:
844     # 0", "link-target:", etc.
845     if not fields:
846         fields = all_fields
847
848     result = []
849     if 'path' in fields:
850         path = meta.path or ''
851         result.append('path: ' + path)
852     if 'mode' in fields:
853         result.append('mode: %s (%s)' % (oct(meta.mode),
854                                          xstat.mode_str(meta.mode)))
855     if 'link-target' in fields and stat.S_ISLNK(meta.mode):
856         result.append('link-target: ' + meta.symlink_target)
857     if 'rdev' in fields:
858         if meta.rdev:
859             result.append('rdev: %d,%d' % (os.major(meta.rdev),
860                                            os.minor(meta.rdev)))
861         else:
862             result.append('rdev: 0')
863     if 'size' in fields and meta.size:
864         result.append('size: ' + str(meta.size))
865     if 'uid' in fields:
866         result.append('uid: ' + str(meta.uid))
867     if 'gid' in fields:
868         result.append('gid: ' + str(meta.gid))
869     if 'user' in fields:
870         result.append('user: ' + meta.user)
871     if 'group' in fields:
872         result.append('group: ' + meta.group)
873     if 'atime' in fields:
874         # If we don't have xstat.lutime, that means we have to use
875         # utime(), and utime() has no way to set the mtime/atime of a
876         # symlink.  Thus, the mtime/atime of a symlink is meaningless,
877         # so let's not report it.  (That way scripts comparing
878         # before/after won't trigger.)
879         if xstat.lutime or not stat.S_ISLNK(meta.mode):
880             result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
881         else:
882             result.append('atime: 0')
883     if 'mtime' in fields:
884         if xstat.lutime or not stat.S_ISLNK(meta.mode):
885             result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
886         else:
887             result.append('mtime: 0')
888     if 'ctime' in fields:
889         result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
890     if 'linux-attr' in fields and meta.linux_attr:
891         result.append('linux-attr: ' + hex(meta.linux_attr))
892     if 'linux-xattr' in fields and meta.linux_xattr:
893         for name, value in meta.linux_xattr:
894             result.append('linux-xattr: %s -> %s' % (name, repr(value)))
895     if 'posix1e-acl' in fields and meta.posix1e_acl and posix1e:
896         flags = posix1e.TEXT_ABBREVIATE
897         if stat.S_ISDIR(meta.mode):
898             acl = meta.posix1e_acl[0]
899             default_acl = meta.posix1e_acl[2]
900             result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
901             result.append(acl.to_any_text('posix1e-acl-default: ', '\n', flags))
902         else:
903             acl = meta.posix1e_acl[0]
904             result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
905     return '\n'.join(result)
906
907
908 class _ArchiveIterator:
909     def next(self):
910         try:
911             return Metadata.read(self._file)
912         except EOFError:
913             raise StopIteration()
914
915     def __iter__(self):
916         return self
917
918     def __init__(self, file):
919         self._file = file
920
921
922 def display_archive(file):
923     if verbose > 1:
924         first_item = True
925         for meta in _ArchiveIterator(file):
926             if not first_item:
927                 print
928             print detailed_str(meta)
929             first_item = False
930     elif verbose > 0:
931         for meta in _ArchiveIterator(file):
932             print summary_str(meta)
933     elif verbose == 0:
934         for meta in _ArchiveIterator(file):
935             if not meta.path:
936                 print >> sys.stderr, \
937                     'bup: no metadata path, but asked to only display path', \
938                     '(increase verbosity?)'
939                 sys.exit(1)
940             print meta.path
941
942
943 def start_extract(file, create_symlinks=True):
944     for meta in _ArchiveIterator(file):
945         if not meta: # Hit end record.
946             break
947         if verbose:
948             print >> sys.stderr, meta.path
949         xpath = _clean_up_extract_path(meta.path)
950         if not xpath:
951             add_error(Exception('skipping risky path "%s"' % meta.path))
952         else:
953             meta.path = xpath
954             _set_up_path(meta, create_symlinks=create_symlinks)
955
956
957 def finish_extract(file, restore_numeric_ids=False):
958     all_dirs = []
959     for meta in _ArchiveIterator(file):
960         if not meta: # Hit end record.
961             break
962         xpath = _clean_up_extract_path(meta.path)
963         if not xpath:
964             add_error(Exception('skipping risky path "%s"' % dir.path))
965         else:
966             if os.path.isdir(meta.path):
967                 all_dirs.append(meta)
968             else:
969                 if verbose:
970                     print >> sys.stderr, meta.path
971                 meta.apply_to_path(path=xpath,
972                                    restore_numeric_ids=restore_numeric_ids)
973     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
974     for dir in all_dirs:
975         # Don't need to check xpath -- won't be in all_dirs if not OK.
976         xpath = _clean_up_extract_path(dir.path)
977         if verbose:
978             print >> sys.stderr, dir.path
979         dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
980
981
982 def extract(file, restore_numeric_ids=False, create_symlinks=True):
983     # For now, just store all the directories and handle them last,
984     # longest first.
985     all_dirs = []
986     for meta in _ArchiveIterator(file):
987         if not meta: # Hit end record.
988             break
989         xpath = _clean_up_extract_path(meta.path)
990         if not xpath:
991             add_error(Exception('skipping risky path "%s"' % meta.path))
992         else:
993             meta.path = xpath
994             if verbose:
995                 print >> sys.stderr, '+', meta.path
996             _set_up_path(meta, create_symlinks=create_symlinks)
997             if os.path.isdir(meta.path):
998                 all_dirs.append(meta)
999             else:
1000                 if verbose:
1001                     print >> sys.stderr, '=', meta.path
1002                 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1003     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1004     for dir in all_dirs:
1005         # Don't need to check xpath -- won't be in all_dirs if not OK.
1006         xpath = _clean_up_extract_path(dir.path)
1007         if verbose:
1008             print >> sys.stderr, '=', xpath
1009         # Shouldn't have to check for risky paths here (omitted above).
1010         dir.apply_to_path(path=dir.path,
1011                           restore_numeric_ids=restore_numeric_ids)