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