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