]> arthur.barton.de Git - bup.git/blob - lib/bup/metadata.py
7f51cfd53300cc069985c1db8812aab2cefab626
[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 in (errno.ENOTEMPTY, errno.EEXIST):
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             try:
316                 os.mknod(path, 0600 | stat.S_IFSOCK)
317             except OSError, e:
318                 if e.errno == errno.EINVAL:
319                     s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
320                     s.bind(path)
321                 else:
322                     raise
323         elif stat.S_ISLNK(self.mode):
324             assert(self._recognized_file_type())
325             if self.symlink_target and create_symlinks:
326                 # on MacOS, symlink() permissions depend on umask, and there's
327                 # no way to chown a symlink after creating it, so we have to
328                 # be careful here!
329                 oldumask = os.umask((self.mode & 0777) ^ 0777)
330                 try:
331                     os.symlink(self.symlink_target, path)
332                 finally:
333                     os.umask(oldumask)
334         # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
335         else:
336             assert(not self._recognized_file_type())
337             add_error('not creating "%s" with unrecognized mode "0x%x"\n'
338                       % (path, self.mode))
339
340     def _apply_common_rec(self, path, restore_numeric_ids=False):
341         if not self.mode:
342             raise ApplyError('no metadata - cannot apply to ' + path)
343
344         # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
345         # EACCES errors at this stage are fatal for the current path.
346         if lutime and stat.S_ISLNK(self.mode):
347             try:
348                 lutime(path, (self.atime, self.mtime))
349             except OSError, e:
350                 if e.errno == errno.EACCES:
351                     raise ApplyError('lutime: %s' % e)
352                 else:
353                     raise
354         else:
355             try:
356                 utime(path, (self.atime, self.mtime))
357             except OSError, e:
358                 if e.errno == errno.EACCES:
359                     raise ApplyError('utime: %s' % e)
360                 else:
361                     raise
362
363         # Implement tar/rsync-like semantics; see bup-restore(1).
364         # FIXME: should we consider caching user/group name <-> id
365         # mappings, getgroups(), etc.?
366         uid = gid = -1 # By default, do nothing.
367         if is_superuser():
368             uid = self.uid
369             gid = self.gid
370             if not restore_numeric_ids:
371                 if self.uid != 0 and self.user:
372                     entry = pwd_from_name(self.user)
373                     if entry:
374                         uid = entry.pw_uid
375                 if self.gid != 0 and self.group:
376                     entry = grp_from_name(self.group)
377                     if entry:
378                         gid = entry.gr_gid
379         else: # not superuser - only consider changing the group/gid
380             user_gids = os.getgroups()
381             if self.gid in user_gids:
382                 gid = self.gid
383             if not restore_numeric_ids and self.gid != 0:
384                 # The grp might not exist on the local system.
385                 grps = filter(None, [grp_from_gid(x) for x in user_gids])
386                 if self.group in [x.gr_name for x in grps]:
387                     g = grp_from_name(self.group)
388                     if g:
389                         gid = g.gr_gid
390
391         if uid != -1 or gid != -1:
392             try:
393                 os.lchown(path, uid, gid)
394             except OSError, e:
395                 if e.errno == errno.EPERM:
396                     add_error('lchown: %s' %  e)
397                 else:
398                     raise
399
400         if _have_lchmod:
401             os.lchmod(path, stat.S_IMODE(self.mode))
402         elif not stat.S_ISLNK(self.mode):
403             os.chmod(path, stat.S_IMODE(self.mode))
404
405
406     ## Path records
407
408     def _encode_path(self):
409         if self.path:
410             return vint.pack('s', self.path)
411         else:
412             return None
413
414     def _load_path_rec(self, port):
415         self.path = vint.unpack('s', vint.read_bvec(port))[0]
416
417
418     ## Symlink targets
419
420     def _add_symlink_target(self, path, st):
421         try:
422             if stat.S_ISLNK(st.st_mode):
423                 self.symlink_target = os.readlink(path)
424         except OSError, e:
425             add_error('readlink: %s', e)
426
427     def _encode_symlink_target(self):
428         return self.symlink_target
429
430     def _load_symlink_target_rec(self, port):
431         self.symlink_target = vint.read_bvec(port)
432
433
434     ## Hardlink targets
435
436     def _add_hardlink_target(self, target):
437         self.hardlink_target = target
438
439     def _same_hardlink_target(self, other):
440         """Return true or false to indicate similarity in the hardlink sense."""
441         return self.hardlink_target == other.hardlink_target
442
443     def _encode_hardlink_target(self):
444         return self.hardlink_target
445
446     def _load_hardlink_target_rec(self, port):
447         self.hardlink_target = vint.read_bvec(port)
448
449
450     ## POSIX1e ACL records
451
452     # Recorded as a list:
453     #   [txt_id_acl, num_id_acl]
454     # or, if a directory:
455     #   [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
456     # The numeric/text distinction only matters when reading/restoring
457     # a stored record.
458     def _add_posix1e_acl(self, path, st):
459         if not posix1e: return
460         if not stat.S_ISLNK(st.st_mode):
461             try:
462                 if posix1e.has_extended(path):
463                     acl = posix1e.ACL(file=path)
464                     self.posix1e_acl = [acl, acl] # txt and num are the same
465                     if stat.S_ISDIR(st.st_mode):
466                         acl = posix1e.ACL(filedef=path)
467                         self.posix1e_acl.extend([acl, acl])
468             except EnvironmentError, e:
469                 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
470                     raise
471
472     def _same_posix1e_acl(self, other):
473         """Return true or false to indicate similarity in the hardlink sense."""
474         return self.posix1e_acl == other.posix1e_acl
475
476     def _encode_posix1e_acl(self):
477         # Encode as two strings (w/default ACL string possibly empty).
478         if self.posix1e_acl:
479             acls = self.posix1e_acl
480             txt_flags = posix1e.TEXT_ABBREVIATE
481             num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
482             acl_reps = [acls[0].to_any_text('', '\n', txt_flags),
483                         acls[1].to_any_text('', '\n', num_flags)]
484             if len(acls) < 3:
485                 acl_reps += ['', '']
486             else:
487                 acl_reps.append(acls[2].to_any_text('', '\n', txt_flags))
488                 acl_reps.append(acls[3].to_any_text('', '\n', num_flags))
489             return vint.pack('ssss',
490                              acl_reps[0], acl_reps[1], acl_reps[2], acl_reps[3])
491         else:
492             return None
493
494     def _load_posix1e_acl_rec(self, port):
495         if not posix1e: return
496         data = vint.read_bvec(port)
497         acl_reps = vint.unpack('ssss', data)
498         if acl_reps[2] == '':
499             acl_reps = acl_reps[:2]
500         self.posix1e_acl = [posix1e.ACL(text=x) for x in acl_reps]
501
502     def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
503         def apply_acl(acl, kind):
504             try:
505                 acl.applyto(path, kind)
506             except IOError, e:
507                 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
508                     raise ApplyError('POSIX1e ACL applyto: %s' % e)
509                 else:
510                     raise
511
512         if not posix1e:
513             if self.posix1e_acl:
514                 add_error("%s: can't restore ACLs; posix1e support missing.\n"
515                           % path)
516             return
517         if self.posix1e_acl:
518             acls = self.posix1e_acl
519             if len(acls) > 2:
520                 if restore_numeric_ids:
521                     apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
522                 else:
523                     apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
524             if restore_numeric_ids:
525                 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
526             else:
527                 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
528
529
530     ## Linux attributes (lsattr(1), chattr(1))
531
532     def _add_linux_attr(self, path, st):
533         if not get_linux_file_attr: return
534         if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
535             try:
536                 attr = get_linux_file_attr(path)
537                 if attr != 0:
538                     self.linux_attr = attr
539             except OSError, e:
540                 if e.errno == errno.EACCES:
541                     add_error('read Linux attr: %s' % e)
542                 elif e.errno == errno.ENOTTY or e.errno == errno.ENOSYS:
543                     # ENOTTY: Function not implemented.
544                     # ENOSYS: Inappropriate ioctl for device.
545                     # Assume filesystem doesn't support attrs.
546                     return
547                 else:
548                     raise
549
550     def _same_linux_attr(self, other):
551         """Return true or false to indicate similarity in the hardlink sense."""
552         return self.linux_attr == other.linux_attr
553
554     def _encode_linux_attr(self):
555         if self.linux_attr:
556             return vint.pack('V', self.linux_attr)
557         else:
558             return None
559
560     def _load_linux_attr_rec(self, port):
561         data = vint.read_bvec(port)
562         self.linux_attr = vint.unpack('V', data)[0]
563
564     def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
565         if self.linux_attr:
566             if not set_linux_file_attr:
567                 add_error("%s: can't restore linuxattrs: "
568                           "linuxattr support missing.\n" % path)
569                 return
570             try:
571                 set_linux_file_attr(path, self.linux_attr)
572             except OSError, e:
573                 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS):
574                     raise ApplyError('Linux chattr: %s' % e)
575                 else:
576                     raise
577
578
579     ## Linux extended attributes (getfattr(1), setfattr(1))
580
581     def _add_linux_xattr(self, path, st):
582         if not xattr: return
583         try:
584             self.linux_xattr = xattr.get_all(path, nofollow=True)
585         except EnvironmentError, e:
586             if e.errno != errno.EOPNOTSUPP:
587                 raise
588
589     def _same_linux_xattr(self, other):
590         """Return true or false to indicate similarity in the hardlink sense."""
591         return self.linux_xattr == other.linux_xattr
592
593     def _encode_linux_xattr(self):
594         if self.linux_xattr:
595             result = vint.pack('V', len(self.linux_xattr))
596             for name, value in self.linux_xattr:
597                 result += vint.pack('ss', name, value)
598             return result
599         else:
600             return None
601
602     def _load_linux_xattr_rec(self, file):
603         data = vint.read_bvec(file)
604         memfile = StringIO(data)
605         result = []
606         for i in range(vint.read_vuint(memfile)):
607             key = vint.read_bvec(memfile)
608             value = vint.read_bvec(memfile)
609             result.append((key, value))
610         self.linux_xattr = result
611
612     def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
613         if not xattr:
614             if self.linux_xattr:
615                 add_error("%s: can't restore xattr; xattr support missing.\n"
616                           % path)
617             return
618         existing_xattrs = set(xattr.list(path, nofollow=True))
619         if self.linux_xattr:
620             for k, v in self.linux_xattr:
621                 if k not in existing_xattrs \
622                         or v != xattr.get(path, k, nofollow=True):
623                     try:
624                         xattr.set(path, k, v, nofollow=True)
625                     except IOError, e:
626                         if e.errno == errno.EPERM \
627                                 or e.errno == errno.EOPNOTSUPP:
628                             raise ApplyError('xattr.set: %s' % e)
629                         else:
630                             raise
631                 existing_xattrs -= frozenset([k])
632             for k in existing_xattrs:
633                 try:
634                     xattr.remove(path, k, nofollow=True)
635                 except IOError, e:
636                     if e.errno == errno.EPERM:
637                         raise ApplyError('xattr.remove: %s' % e)
638                     else:
639                         raise
640
641     def __init__(self):
642         self.mode = None
643         # optional members
644         self.path = None
645         self.size = None
646         self.symlink_target = None
647         self.hardlink_target = None
648         self.linux_attr = None
649         self.linux_xattr = None
650         self.posix1e_acl = None
651         self.posix1e_acl_default = None
652
653     def write(self, port, include_path=True):
654         records = include_path and [(_rec_tag_path, self._encode_path())] or []
655         records.extend([(_rec_tag_common, self._encode_common()),
656                         (_rec_tag_symlink_target,
657                          self._encode_symlink_target()),
658                         (_rec_tag_hardlink_target,
659                          self._encode_hardlink_target()),
660                         (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
661                         (_rec_tag_linux_attr, self._encode_linux_attr()),
662                         (_rec_tag_linux_xattr, self._encode_linux_xattr())])
663         for tag, data in records:
664             if data:
665                 vint.write_vuint(port, tag)
666                 vint.write_bvec(port, data)
667         vint.write_vuint(port, _rec_tag_end)
668
669     def encode(self, include_path=True):
670         port = StringIO()
671         self.write(port, include_path)
672         return port.getvalue()
673
674     @staticmethod
675     def read(port):
676         # This method should either return a valid Metadata object,
677         # return None if there was no information at all (just a
678         # _rec_tag_end), throw EOFError if there was nothing at all to
679         # read, or throw an Exception if a valid object could not be
680         # read completely.
681         tag = vint.read_vuint(port)
682         if tag == _rec_tag_end:
683             return None
684         try: # From here on, EOF is an error.
685             result = Metadata()
686             while True: # only exit is error (exception) or _rec_tag_end
687                 if tag == _rec_tag_path:
688                     result._load_path_rec(port)
689                 elif tag == _rec_tag_common:
690                     result._load_common_rec(port)
691                 elif tag == _rec_tag_symlink_target:
692                     result._load_symlink_target_rec(port)
693                 elif tag == _rec_tag_hardlink_target:
694                     result._load_hardlink_target_rec(port)
695                 elif tag == _rec_tag_posix1e_acl:
696                     result._load_posix1e_acl_rec(port)
697                 elif tag == _rec_tag_nfsv4_acl:
698                     result._load_nfsv4_acl_rec(port)
699                 elif tag == _rec_tag_linux_attr:
700                     result._load_linux_attr_rec(port)
701                 elif tag == _rec_tag_linux_xattr:
702                     result._load_linux_xattr_rec(port)
703                 elif tag == _rec_tag_end:
704                     return result
705                 else: # unknown record
706                     vint.skip_bvec(port)
707                 tag = vint.read_vuint(port)
708         except EOFError:
709             raise Exception("EOF while reading Metadata")
710
711     def isdir(self):
712         return stat.S_ISDIR(self.mode)
713
714     def create_path(self, path, create_symlinks=True):
715         self._create_via_common_rec(path, create_symlinks=create_symlinks)
716
717     def apply_to_path(self, path=None, restore_numeric_ids=False):
718         # apply metadata to path -- file must exist
719         if not path:
720             path = self.path
721         if not path:
722             raise Exception('Metadata.apply_to_path() called with no path')
723         if not self._recognized_file_type():
724             add_error('not applying metadata to "%s"' % path
725                       + ' with unrecognized mode "0x%x"\n' % self.mode)
726             return
727         num_ids = restore_numeric_ids
728         try:
729             self._apply_common_rec(path, restore_numeric_ids=num_ids)
730             self._apply_posix1e_acl_rec(path, restore_numeric_ids=num_ids)
731             self._apply_linux_attr_rec(path, restore_numeric_ids=num_ids)
732             self._apply_linux_xattr_rec(path, restore_numeric_ids=num_ids)
733         except ApplyError, e:
734             add_error(e)
735
736     def same_file(self, other):
737         """Compare this to other for equivalency.  Return true if
738         their information implies they could represent the same file
739         on disk, in the hardlink sense.  Assume they're both regular
740         files."""
741         return self._same_common(other) \
742             and self._same_hardlink_target(other) \
743             and self._same_posix1e_acl(other) \
744             and self._same_linux_attr(other) \
745             and self._same_linux_xattr(other)
746
747
748 def from_path(path, statinfo=None, archive_path=None,
749               save_symlinks=True, hardlink_target=None):
750     result = Metadata()
751     result.path = archive_path
752     st = statinfo or xstat.lstat(path)
753     result.size = st.st_size
754     result._add_common(path, st)
755     if save_symlinks:
756         result._add_symlink_target(path, st)
757     result._add_hardlink_target(hardlink_target)
758     result._add_posix1e_acl(path, st)
759     result._add_linux_attr(path, st)
760     result._add_linux_xattr(path, st)
761     return result
762
763
764 def save_tree(output_file, paths,
765               recurse=False,
766               write_paths=True,
767               save_symlinks=True,
768               xdev=False):
769
770     # Issue top-level rewrite warnings.
771     for path in paths:
772         safe_path = _clean_up_path_for_archive(path)
773         if safe_path != path:
774             log('archiving "%s" as "%s"\n' % (path, safe_path))
775
776     start_dir = os.getcwd()
777     try:
778         for (p, st) in recursive_dirlist(paths, xdev=xdev):
779             dirlist_dir = os.getcwd()
780             os.chdir(start_dir)
781             safe_path = _clean_up_path_for_archive(p)
782             m = from_path(p, statinfo=st, archive_path=safe_path,
783                           save_symlinks=save_symlinks)
784             if verbose:
785                 print >> sys.stderr, m.path
786             m.write(output_file, include_path=write_paths)
787             os.chdir(dirlist_dir)
788     finally:
789         os.chdir(start_dir)
790
791
792 def _set_up_path(meta, create_symlinks=True):
793     # Allow directories to exist as a special case -- might have
794     # been created by an earlier longer path.
795     if meta.isdir():
796         mkdirp(meta.path)
797     else:
798         parent = os.path.dirname(meta.path)
799         if parent:
800             mkdirp(parent)
801         meta.create_path(meta.path, create_symlinks=create_symlinks)
802
803
804 all_fields = frozenset(['path',
805                         'mode',
806                         'link-target',
807                         'rdev',
808                         'size',
809                         'uid',
810                         'gid',
811                         'user',
812                         'group',
813                         'atime',
814                         'mtime',
815                         'ctime',
816                         'linux-attr',
817                         'linux-xattr',
818                         'posix1e-acl'])
819
820
821 def summary_str(meta):
822     mode_val = xstat.mode_str(meta.mode)
823     user_val = meta.user
824     if not user_val:
825         user_val = str(meta.uid)
826     group_val = meta.group
827     if not group_val:
828         group_val = str(meta.gid)
829     size_or_dev_val = '-'
830     if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
831         size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
832     elif meta.size:
833         size_or_dev_val = meta.size
834     mtime_secs = xstat.fstime_floor_secs(meta.mtime)
835     time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
836     path_val = meta.path or ''
837     if stat.S_ISLNK(meta.mode):
838         path_val += ' -> ' + meta.symlink_target
839     return '%-10s %-11s %11s %16s %s' % (mode_val,
840                                          user_val + "/" + group_val,
841                                          size_or_dev_val,
842                                          time_val,
843                                          path_val)
844
845
846 def detailed_str(meta, fields = None):
847     # FIXME: should optional fields be omitted, or empty i.e. "rdev:
848     # 0", "link-target:", etc.
849     if not fields:
850         fields = all_fields
851
852     result = []
853     if 'path' in fields:
854         path = meta.path or ''
855         result.append('path: ' + path)
856     if 'mode' in fields:
857         result.append('mode: %s (%s)' % (oct(meta.mode),
858                                          xstat.mode_str(meta.mode)))
859     if 'link-target' in fields and stat.S_ISLNK(meta.mode):
860         result.append('link-target: ' + meta.symlink_target)
861     if 'rdev' in fields:
862         if meta.rdev:
863             result.append('rdev: %d,%d' % (os.major(meta.rdev),
864                                            os.minor(meta.rdev)))
865         else:
866             result.append('rdev: 0')
867     if 'size' in fields and meta.size:
868         result.append('size: ' + str(meta.size))
869     if 'uid' in fields:
870         result.append('uid: ' + str(meta.uid))
871     if 'gid' in fields:
872         result.append('gid: ' + str(meta.gid))
873     if 'user' in fields:
874         result.append('user: ' + meta.user)
875     if 'group' in fields:
876         result.append('group: ' + meta.group)
877     if 'atime' in fields:
878         # If we don't have xstat.lutime, that means we have to use
879         # utime(), and utime() has no way to set the mtime/atime of a
880         # symlink.  Thus, the mtime/atime of a symlink is meaningless,
881         # so let's not report it.  (That way scripts comparing
882         # before/after won't trigger.)
883         if xstat.lutime or not stat.S_ISLNK(meta.mode):
884             result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
885         else:
886             result.append('atime: 0')
887     if 'mtime' in fields:
888         if xstat.lutime or not stat.S_ISLNK(meta.mode):
889             result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
890         else:
891             result.append('mtime: 0')
892     if 'ctime' in fields:
893         result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
894     if 'linux-attr' in fields and meta.linux_attr:
895         result.append('linux-attr: ' + hex(meta.linux_attr))
896     if 'linux-xattr' in fields and meta.linux_xattr:
897         for name, value in meta.linux_xattr:
898             result.append('linux-xattr: %s -> %s' % (name, repr(value)))
899     if 'posix1e-acl' in fields and meta.posix1e_acl and posix1e:
900         flags = posix1e.TEXT_ABBREVIATE
901         if stat.S_ISDIR(meta.mode):
902             acl = meta.posix1e_acl[0]
903             default_acl = meta.posix1e_acl[2]
904             result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
905             result.append(acl.to_any_text('posix1e-acl-default: ', '\n', flags))
906         else:
907             acl = meta.posix1e_acl[0]
908             result.append(acl.to_any_text('posix1e-acl: ', '\n', flags))
909     return '\n'.join(result)
910
911
912 class _ArchiveIterator:
913     def next(self):
914         try:
915             return Metadata.read(self._file)
916         except EOFError:
917             raise StopIteration()
918
919     def __iter__(self):
920         return self
921
922     def __init__(self, file):
923         self._file = file
924
925
926 def display_archive(file):
927     if verbose > 1:
928         first_item = True
929         for meta in _ArchiveIterator(file):
930             if not first_item:
931                 print
932             print detailed_str(meta)
933             first_item = False
934     elif verbose > 0:
935         for meta in _ArchiveIterator(file):
936             print summary_str(meta)
937     elif verbose == 0:
938         for meta in _ArchiveIterator(file):
939             if not meta.path:
940                 print >> sys.stderr, \
941                     'bup: no metadata path, but asked to only display path', \
942                     '(increase verbosity?)'
943                 sys.exit(1)
944             print meta.path
945
946
947 def start_extract(file, create_symlinks=True):
948     for meta in _ArchiveIterator(file):
949         if not meta: # Hit end record.
950             break
951         if verbose:
952             print >> sys.stderr, meta.path
953         xpath = _clean_up_extract_path(meta.path)
954         if not xpath:
955             add_error(Exception('skipping risky path "%s"' % meta.path))
956         else:
957             meta.path = xpath
958             _set_up_path(meta, create_symlinks=create_symlinks)
959
960
961 def finish_extract(file, restore_numeric_ids=False):
962     all_dirs = []
963     for meta in _ArchiveIterator(file):
964         if not meta: # Hit end record.
965             break
966         xpath = _clean_up_extract_path(meta.path)
967         if not xpath:
968             add_error(Exception('skipping risky path "%s"' % dir.path))
969         else:
970             if os.path.isdir(meta.path):
971                 all_dirs.append(meta)
972             else:
973                 if verbose:
974                     print >> sys.stderr, meta.path
975                 meta.apply_to_path(path=xpath,
976                                    restore_numeric_ids=restore_numeric_ids)
977     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
978     for dir in all_dirs:
979         # Don't need to check xpath -- won't be in all_dirs if not OK.
980         xpath = _clean_up_extract_path(dir.path)
981         if verbose:
982             print >> sys.stderr, dir.path
983         dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
984
985
986 def extract(file, restore_numeric_ids=False, create_symlinks=True):
987     # For now, just store all the directories and handle them last,
988     # longest first.
989     all_dirs = []
990     for meta in _ArchiveIterator(file):
991         if not meta: # Hit end record.
992             break
993         xpath = _clean_up_extract_path(meta.path)
994         if not xpath:
995             add_error(Exception('skipping risky path "%s"' % meta.path))
996         else:
997             meta.path = xpath
998             if verbose:
999                 print >> sys.stderr, '+', meta.path
1000             _set_up_path(meta, create_symlinks=create_symlinks)
1001             if os.path.isdir(meta.path):
1002                 all_dirs.append(meta)
1003             else:
1004                 if verbose:
1005                     print >> sys.stderr, '=', meta.path
1006                 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1007     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1008     for dir in all_dirs:
1009         # Don't need to check xpath -- won't be in all_dirs if not OK.
1010         xpath = _clean_up_extract_path(dir.path)
1011         if verbose:
1012             print >> sys.stderr, '=', xpath
1013         # Shouldn't have to check for risky paths here (omitted above).
1014         dir.apply_to_path(path=dir.path,
1015                           restore_numeric_ids=restore_numeric_ids)