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