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