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