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