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