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