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