]> arthur.barton.de Git - bup.git/blob - lib/bup/metadata.py
Ignore lchmod() ENOSYS (i.e. function not implemented)
[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             try:
427                 os.lchmod(path, stat.S_IMODE(self.mode))
428             except errno.ENOSYS:  # Function not implemented
429                 pass
430         elif not stat.S_ISLNK(self.mode):
431             os.chmod(path, stat.S_IMODE(self.mode))
432
433
434     ## Path records
435
436     def _encode_path(self):
437         if self.path:
438             return vint.pack('s', self.path)
439         else:
440             return None
441
442     def _load_path_rec(self, port):
443         self.path = vint.unpack('s', vint.read_bvec(port))[0]
444
445
446     ## Symlink targets
447
448     def _add_symlink_target(self, path, st):
449         try:
450             if stat.S_ISLNK(st.st_mode):
451                 self.symlink_target = os.readlink(path)
452         except OSError, e:
453             add_error('readlink: %s' % e)
454
455     def _encode_symlink_target(self):
456         return self.symlink_target
457
458     def _load_symlink_target_rec(self, port):
459         self.symlink_target = vint.read_bvec(port)
460
461
462     ## Hardlink targets
463
464     def _add_hardlink_target(self, target):
465         self.hardlink_target = target
466
467     def _same_hardlink_target(self, other):
468         """Return true or false to indicate similarity in the hardlink sense."""
469         return self.hardlink_target == other.hardlink_target
470
471     def _encode_hardlink_target(self):
472         return self.hardlink_target
473
474     def _load_hardlink_target_rec(self, port):
475         self.hardlink_target = vint.read_bvec(port)
476
477
478     ## POSIX1e ACL records
479
480     # Recorded as a list:
481     #   [txt_id_acl, num_id_acl]
482     # or, if a directory:
483     #   [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
484     # The numeric/text distinction only matters when reading/restoring
485     # a stored record.
486     def _add_posix1e_acl(self, path, st):
487         if not posix1e: return
488         if not stat.S_ISLNK(st.st_mode):
489             acls = None
490             def_acls = None
491             try:
492                 if posix1e.has_extended(path):
493                     acl = posix1e.ACL(file=path)
494                     acls = [acl, acl] # txt and num are the same
495                     if stat.S_ISDIR(st.st_mode):
496                         def_acl = posix1e.ACL(filedef=path)
497                         def_acls = [def_acl, def_acl]
498             except EnvironmentError, e:
499                 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
500                     raise
501             if acls:
502                 txt_flags = posix1e.TEXT_ABBREVIATE
503                 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
504                 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
505                            acls[1].to_any_text('', '\n', num_flags)]
506                 if def_acls:
507                     acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
508                     acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
509                 self.posix1e_acl = acl_rep
510
511     def _same_posix1e_acl(self, other):
512         """Return true or false to indicate similarity in the hardlink sense."""
513         return self.posix1e_acl == other.posix1e_acl
514
515     def _encode_posix1e_acl(self):
516         # Encode as two strings (w/default ACL string possibly empty).
517         if self.posix1e_acl:
518             acls = self.posix1e_acl
519             if len(acls) == 2:
520                 acls.extend(['', ''])
521             return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
522         else:
523             return None
524
525     def _load_posix1e_acl_rec(self, port):
526         acl_rep = vint.unpack('ssss', vint.read_bvec(port))
527         if acl_rep[2] == '':
528             acl_rep = acl_rep[:2]
529         self.posix1e_acl = acl_rep
530
531     def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
532         def apply_acl(acl_rep, kind):
533             try:
534                 acl = posix1e.ACL(text = acl_rep)
535             except IOError, e:
536                 if e.errno == 0:
537                     # pylibacl appears to return an IOError with errno
538                     # set to 0 if a group referred to by the ACL rep
539                     # doesn't exist on the current system.
540                     raise ApplyError("POSIX1e ACL: can't create %r for %r"
541                                      % (acl_rep, path))
542                 else:
543                     raise
544             try:
545                 acl.applyto(path, kind)
546             except IOError, e:
547                 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
548                     raise ApplyError('POSIX1e ACL applyto: %s' % e)
549                 else:
550                     raise
551
552         if not posix1e:
553             if self.posix1e_acl:
554                 add_error("%s: can't restore ACLs; posix1e support missing.\n"
555                           % path)
556             return
557         if self.posix1e_acl:
558             acls = self.posix1e_acl
559             if len(acls) > 2:
560                 if restore_numeric_ids:
561                     apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
562                 else:
563                     apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
564             if restore_numeric_ids:
565                 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
566             else:
567                 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
568
569
570     ## Linux attributes (lsattr(1), chattr(1))
571
572     def _add_linux_attr(self, path, st):
573         check_linux_file_attr_api()
574         if not get_linux_file_attr: return
575         if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
576             try:
577                 attr = get_linux_file_attr(path)
578                 if attr != 0:
579                     self.linux_attr = attr
580             except OSError, e:
581                 if e.errno == errno.EACCES:
582                     add_error('read Linux attr: %s' % e)
583                 elif e.errno in (errno.ENOTTY, errno.ENOSYS, errno.EOPNOTSUPP):
584                     # Assume filesystem doesn't support attrs.
585                     return
586                 else:
587                     raise
588
589     def _same_linux_attr(self, other):
590         """Return true or false to indicate similarity in the hardlink sense."""
591         return self.linux_attr == other.linux_attr
592
593     def _encode_linux_attr(self):
594         if self.linux_attr:
595             return vint.pack('V', self.linux_attr)
596         else:
597             return None
598
599     def _load_linux_attr_rec(self, port):
600         data = vint.read_bvec(port)
601         self.linux_attr = vint.unpack('V', data)[0]
602
603     def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
604         if self.linux_attr:
605             check_linux_file_attr_api()
606             if not set_linux_file_attr:
607                 add_error("%s: can't restore linuxattrs: "
608                           "linuxattr support missing.\n" % path)
609                 return
610             try:
611                 set_linux_file_attr(path, self.linux_attr)
612             except OSError, e:
613                 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS,
614                                errno.EACCES):
615                     raise ApplyError('Linux chattr: %s (0x%s)'
616                                      % (e, hex(self.linux_attr)))
617                 else:
618                     raise
619
620
621     ## Linux extended attributes (getfattr(1), setfattr(1))
622
623     def _add_linux_xattr(self, path, st):
624         if not xattr: return
625         try:
626             self.linux_xattr = xattr.get_all(path, nofollow=True)
627         except EnvironmentError, e:
628             if e.errno != errno.EOPNOTSUPP:
629                 raise
630
631     def _same_linux_xattr(self, other):
632         """Return true or false to indicate similarity in the hardlink sense."""
633         return self.linux_xattr == other.linux_xattr
634
635     def _encode_linux_xattr(self):
636         if self.linux_xattr:
637             result = vint.pack('V', len(self.linux_xattr))
638             for name, value in self.linux_xattr:
639                 result += vint.pack('ss', name, value)
640             return result
641         else:
642             return None
643
644     def _load_linux_xattr_rec(self, file):
645         data = vint.read_bvec(file)
646         memfile = StringIO(data)
647         result = []
648         for i in range(vint.read_vuint(memfile)):
649             key = vint.read_bvec(memfile)
650             value = vint.read_bvec(memfile)
651             result.append((key, value))
652         self.linux_xattr = result
653
654     def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
655         if not xattr:
656             if self.linux_xattr:
657                 add_error("%s: can't restore xattr; xattr support missing.\n"
658                           % path)
659             return
660         if not self.linux_xattr:
661             return
662         try:
663             existing_xattrs = set(xattr.list(path, nofollow=True))
664         except IOError, e:
665             if e.errno == errno.EACCES:
666                 raise ApplyError('xattr.set %r: %s' % (path, e))
667             else:
668                 raise
669         for k, v in self.linux_xattr:
670             if k not in existing_xattrs \
671                     or v != xattr.get(path, k, nofollow=True):
672                 try:
673                     xattr.set(path, k, v, nofollow=True)
674                 except IOError, e:
675                     if e.errno == errno.EPERM \
676                             or e.errno == errno.EOPNOTSUPP:
677                         raise ApplyError('xattr.set %r: %s' % (path, e))
678                     else:
679                         raise
680             existing_xattrs -= frozenset([k])
681         for k in existing_xattrs:
682             try:
683                 xattr.remove(path, k, nofollow=True)
684             except IOError, e:
685                 if e.errno == errno.EPERM:
686                     raise ApplyError('xattr.remove %r: %s' % (path, e))
687                 else:
688                     raise
689
690     def __init__(self):
691         self.mode = self.uid = self.gid = self.user = self.group = None
692         self.atime = self.mtime = self.ctime = None
693         # optional members
694         self.path = None
695         self.size = None
696         self.symlink_target = None
697         self.hardlink_target = None
698         self.linux_attr = None
699         self.linux_xattr = None
700         self.posix1e_acl = None
701
702     def __repr__(self):
703         result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
704         if self.path:
705             result += ' path:' + repr(self.path)
706         if self.mode:
707             result += ' mode:' + repr(xstat.mode_str(self.mode)
708                                       + '(%s)' % hex(self.mode))
709         if self.uid:
710             result += ' uid:' + str(self.uid)
711         if self.gid:
712             result += ' gid:' + str(self.gid)
713         if self.user:
714             result += ' user:' + repr(self.user)
715         if self.group:
716             result += ' group:' + repr(self.group)
717         if self.size:
718             result += ' size:' + repr(self.size)
719         for name, val in (('atime', self.atime),
720                           ('mtime', self.mtime),
721                           ('ctime', self.ctime)):
722             result += ' %s:%r' \
723                 % (name,
724                    time.strftime('%Y-%m-%d %H:%M %z',
725                                  time.gmtime(xstat.fstime_floor_secs(val))))
726         result += '>'
727         return ''.join(result)
728
729     def write(self, port, include_path=True):
730         records = include_path and [(_rec_tag_path, self._encode_path())] or []
731         records.extend([(_rec_tag_common_v2, self._encode_common()),
732                         (_rec_tag_symlink_target,
733                          self._encode_symlink_target()),
734                         (_rec_tag_hardlink_target,
735                          self._encode_hardlink_target()),
736                         (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
737                         (_rec_tag_linux_attr, self._encode_linux_attr()),
738                         (_rec_tag_linux_xattr, self._encode_linux_xattr())])
739         for tag, data in records:
740             if data:
741                 vint.write_vuint(port, tag)
742                 vint.write_bvec(port, data)
743         vint.write_vuint(port, _rec_tag_end)
744
745     def encode(self, include_path=True):
746         port = StringIO()
747         self.write(port, include_path)
748         return port.getvalue()
749
750     @staticmethod
751     def read(port):
752         # This method should either return a valid Metadata object,
753         # return None if there was no information at all (just a
754         # _rec_tag_end), throw EOFError if there was nothing at all to
755         # read, or throw an Exception if a valid object could not be
756         # read completely.
757         tag = vint.read_vuint(port)
758         if tag == _rec_tag_end:
759             return None
760         try: # From here on, EOF is an error.
761             result = Metadata()
762             while True: # only exit is error (exception) or _rec_tag_end
763                 if tag == _rec_tag_path:
764                     result._load_path_rec(port)
765                 elif tag == _rec_tag_common_v2:
766                     result._load_common_rec(port)
767                 elif tag == _rec_tag_symlink_target:
768                     result._load_symlink_target_rec(port)
769                 elif tag == _rec_tag_hardlink_target:
770                     result._load_hardlink_target_rec(port)
771                 elif tag == _rec_tag_posix1e_acl:
772                     result._load_posix1e_acl_rec(port)
773                 elif tag == _rec_tag_linux_attr:
774                     result._load_linux_attr_rec(port)
775                 elif tag == _rec_tag_linux_xattr:
776                     result._load_linux_xattr_rec(port)
777                 elif tag == _rec_tag_end:
778                     return result
779                 elif tag == _rec_tag_common: # Should be very rare.
780                     result._load_common_rec(port, legacy_format = True)
781                 else: # unknown record
782                     vint.skip_bvec(port)
783                 tag = vint.read_vuint(port)
784         except EOFError:
785             raise Exception("EOF while reading Metadata")
786
787     def isdir(self):
788         return stat.S_ISDIR(self.mode)
789
790     def create_path(self, path, create_symlinks=True):
791         self._create_via_common_rec(path, create_symlinks=create_symlinks)
792
793     def apply_to_path(self, path=None, restore_numeric_ids=False):
794         # apply metadata to path -- file must exist
795         if not path:
796             path = self.path
797         if not path:
798             raise Exception('Metadata.apply_to_path() called with no path')
799         if not self._recognized_file_type():
800             add_error('not applying metadata to "%s"' % path
801                       + ' with unrecognized mode "0x%x"\n' % self.mode)
802             return
803         num_ids = restore_numeric_ids
804         for apply_metadata in (self._apply_common_rec,
805                                self._apply_posix1e_acl_rec,
806                                self._apply_linux_attr_rec,
807                                self._apply_linux_xattr_rec):
808             try:
809                 apply_metadata(path, restore_numeric_ids=num_ids)
810             except ApplyError, e:
811                 add_error(e)
812
813     def same_file(self, other):
814         """Compare this to other for equivalency.  Return true if
815         their information implies they could represent the same file
816         on disk, in the hardlink sense.  Assume they're both regular
817         files."""
818         return self._same_common(other) \
819             and self._same_hardlink_target(other) \
820             and self._same_posix1e_acl(other) \
821             and self._same_linux_attr(other) \
822             and self._same_linux_xattr(other)
823
824
825 def from_path(path, statinfo=None, archive_path=None,
826               save_symlinks=True, hardlink_target=None):
827     result = Metadata()
828     result.path = archive_path
829     st = statinfo or xstat.lstat(path)
830     result.size = st.st_size
831     result._add_common(path, st)
832     if save_symlinks:
833         result._add_symlink_target(path, st)
834     result._add_hardlink_target(hardlink_target)
835     result._add_posix1e_acl(path, st)
836     result._add_linux_attr(path, st)
837     result._add_linux_xattr(path, st)
838     return result
839
840
841 def save_tree(output_file, paths,
842               recurse=False,
843               write_paths=True,
844               save_symlinks=True,
845               xdev=False):
846
847     # Issue top-level rewrite warnings.
848     for path in paths:
849         safe_path = _clean_up_path_for_archive(path)
850         if safe_path != path:
851             log('archiving "%s" as "%s"\n' % (path, safe_path))
852
853     if not recurse:
854         for p in paths:
855             safe_path = _clean_up_path_for_archive(p)
856             st = xstat.lstat(p)
857             if stat.S_ISDIR(st.st_mode):
858                 safe_path += '/'
859             m = from_path(p, statinfo=st, archive_path=safe_path,
860                           save_symlinks=save_symlinks)
861             if verbose:
862                 print >> sys.stderr, m.path
863             m.write(output_file, include_path=write_paths)
864     else:
865         start_dir = os.getcwd()
866         try:
867             for (p, st) in recursive_dirlist(paths, xdev=xdev):
868                 dirlist_dir = os.getcwd()
869                 os.chdir(start_dir)
870                 safe_path = _clean_up_path_for_archive(p)
871                 m = from_path(p, statinfo=st, archive_path=safe_path,
872                               save_symlinks=save_symlinks)
873                 if verbose:
874                     print >> sys.stderr, m.path
875                 m.write(output_file, include_path=write_paths)
876                 os.chdir(dirlist_dir)
877         finally:
878             os.chdir(start_dir)
879
880
881 def _set_up_path(meta, create_symlinks=True):
882     # Allow directories to exist as a special case -- might have
883     # been created by an earlier longer path.
884     if meta.isdir():
885         mkdirp(meta.path)
886     else:
887         parent = os.path.dirname(meta.path)
888         if parent:
889             mkdirp(parent)
890         meta.create_path(meta.path, create_symlinks=create_symlinks)
891
892
893 all_fields = frozenset(['path',
894                         'mode',
895                         'link-target',
896                         'rdev',
897                         'size',
898                         'uid',
899                         'gid',
900                         'user',
901                         'group',
902                         'atime',
903                         'mtime',
904                         'ctime',
905                         'linux-attr',
906                         'linux-xattr',
907                         'posix1e-acl'])
908
909
910 def summary_str(meta, numeric_ids = False, classification = None,
911                 human_readable = False):
912
913     """Return a string containing the "ls -l" style listing for meta.
914     Classification may be "all", "type", or None."""
915     user_str = group_str = size_or_dev_str = '?'
916     symlink_target = None
917     if meta:
918         name = meta.path
919         mode_str = xstat.mode_str(meta.mode)
920         symlink_target = meta.symlink_target
921         mtime_secs = xstat.fstime_floor_secs(meta.mtime)
922         mtime_str = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
923         if meta.user and not numeric_ids:
924             user_str = meta.user
925         elif meta.uid != None:
926             user_str = str(meta.uid)
927         if meta.group and not numeric_ids:
928             group_str = meta.group
929         elif meta.gid != None:
930             group_str = str(meta.gid)
931         if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
932             if meta.rdev:
933                 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
934                                              os.minor(meta.rdev))
935         elif meta.size != None:
936             if human_readable:
937                 size_or_dev_str = format_filesize(meta.size)
938             else:
939                 size_or_dev_str = str(meta.size)
940         else:
941             size_or_dev_str = '-'
942         if classification:
943             classification_str = \
944                 xstat.classification_str(meta.mode, classification == 'all')
945     else:
946         mode_str = '?' * 10
947         mtime_str = '????-??-?? ??:??'
948         classification_str = '?'
949
950     name = name or ''
951     if classification:
952         name += classification_str
953     if symlink_target:
954         name += ' -> ' + meta.symlink_target
955
956     return '%-10s %-11s %11s %16s %s' % (mode_str,
957                                          user_str + "/" + group_str,
958                                          size_or_dev_str,
959                                          mtime_str,
960                                          name)
961
962
963 def detailed_str(meta, fields = None):
964     # FIXME: should optional fields be omitted, or empty i.e. "rdev:
965     # 0", "link-target:", etc.
966     if not fields:
967         fields = all_fields
968
969     result = []
970     if 'path' in fields:
971         path = meta.path or ''
972         result.append('path: ' + path)
973     if 'mode' in fields:
974         result.append('mode: %s (%s)' % (oct(meta.mode),
975                                          xstat.mode_str(meta.mode)))
976     if 'link-target' in fields and stat.S_ISLNK(meta.mode):
977         result.append('link-target: ' + meta.symlink_target)
978     if 'rdev' in fields:
979         if meta.rdev:
980             result.append('rdev: %d,%d' % (os.major(meta.rdev),
981                                            os.minor(meta.rdev)))
982         else:
983             result.append('rdev: 0')
984     if 'size' in fields and meta.size:
985         result.append('size: ' + str(meta.size))
986     if 'uid' in fields:
987         result.append('uid: ' + str(meta.uid))
988     if 'gid' in fields:
989         result.append('gid: ' + str(meta.gid))
990     if 'user' in fields:
991         result.append('user: ' + meta.user)
992     if 'group' in fields:
993         result.append('group: ' + meta.group)
994     if 'atime' in fields:
995         # If we don't have xstat.lutime, that means we have to use
996         # utime(), and utime() has no way to set the mtime/atime of a
997         # symlink.  Thus, the mtime/atime of a symlink is meaningless,
998         # so let's not report it.  (That way scripts comparing
999         # before/after won't trigger.)
1000         if xstat.lutime or not stat.S_ISLNK(meta.mode):
1001             result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1002         else:
1003             result.append('atime: 0')
1004     if 'mtime' in fields:
1005         if xstat.lutime or not stat.S_ISLNK(meta.mode):
1006             result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1007         else:
1008             result.append('mtime: 0')
1009     if 'ctime' in fields:
1010         result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1011     if 'linux-attr' in fields and meta.linux_attr:
1012         result.append('linux-attr: ' + hex(meta.linux_attr))
1013     if 'linux-xattr' in fields and meta.linux_xattr:
1014         for name, value in meta.linux_xattr:
1015             result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1016     if 'posix1e-acl' in fields and meta.posix1e_acl:
1017         acl = meta.posix1e_acl[0]
1018         result.append('posix1e-acl: ' + acl + '\n')
1019         if stat.S_ISDIR(meta.mode):
1020             def_acl = meta.posix1e_acl[2]
1021             result.append('posix1e-acl-default: ' + def_acl + '\n')
1022     return '\n'.join(result)
1023
1024
1025 class _ArchiveIterator:
1026     def next(self):
1027         try:
1028             return Metadata.read(self._file)
1029         except EOFError:
1030             raise StopIteration()
1031
1032     def __iter__(self):
1033         return self
1034
1035     def __init__(self, file):
1036         self._file = file
1037
1038
1039 def display_archive(file):
1040     if verbose > 1:
1041         first_item = True
1042         for meta in _ArchiveIterator(file):
1043             if not first_item:
1044                 print
1045             print detailed_str(meta)
1046             first_item = False
1047     elif verbose > 0:
1048         for meta in _ArchiveIterator(file):
1049             print summary_str(meta)
1050     elif verbose == 0:
1051         for meta in _ArchiveIterator(file):
1052             if not meta.path:
1053                 print >> sys.stderr, \
1054                     'bup: no metadata path, but asked to only display path', \
1055                     '(increase verbosity?)'
1056                 sys.exit(1)
1057             print meta.path
1058
1059
1060 def start_extract(file, create_symlinks=True):
1061     for meta in _ArchiveIterator(file):
1062         if not meta: # Hit end record.
1063             break
1064         if verbose:
1065             print >> sys.stderr, meta.path
1066         xpath = _clean_up_extract_path(meta.path)
1067         if not xpath:
1068             add_error(Exception('skipping risky path "%s"' % meta.path))
1069         else:
1070             meta.path = xpath
1071             _set_up_path(meta, create_symlinks=create_symlinks)
1072
1073
1074 def finish_extract(file, restore_numeric_ids=False):
1075     all_dirs = []
1076     for meta in _ArchiveIterator(file):
1077         if not meta: # Hit end record.
1078             break
1079         xpath = _clean_up_extract_path(meta.path)
1080         if not xpath:
1081             add_error(Exception('skipping risky path "%s"' % dir.path))
1082         else:
1083             if os.path.isdir(meta.path):
1084                 all_dirs.append(meta)
1085             else:
1086                 if verbose:
1087                     print >> sys.stderr, meta.path
1088                 meta.apply_to_path(path=xpath,
1089                                    restore_numeric_ids=restore_numeric_ids)
1090     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1091     for dir in all_dirs:
1092         # Don't need to check xpath -- won't be in all_dirs if not OK.
1093         xpath = _clean_up_extract_path(dir.path)
1094         if verbose:
1095             print >> sys.stderr, dir.path
1096         dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1097
1098
1099 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1100     # For now, just store all the directories and handle them last,
1101     # longest first.
1102     all_dirs = []
1103     for meta in _ArchiveIterator(file):
1104         if not meta: # Hit end record.
1105             break
1106         xpath = _clean_up_extract_path(meta.path)
1107         if not xpath:
1108             add_error(Exception('skipping risky path "%s"' % meta.path))
1109         else:
1110             meta.path = xpath
1111             if verbose:
1112                 print >> sys.stderr, '+', meta.path
1113             _set_up_path(meta, create_symlinks=create_symlinks)
1114             if os.path.isdir(meta.path):
1115                 all_dirs.append(meta)
1116             else:
1117                 if verbose:
1118                     print >> sys.stderr, '=', meta.path
1119                 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1120     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1121     for dir in all_dirs:
1122         # Don't need to check xpath -- won't be in all_dirs if not OK.
1123         xpath = _clean_up_extract_path(dir.path)
1124         if verbose:
1125             print >> sys.stderr, '=', xpath
1126         # Shouldn't have to check for risky paths here (omitted above).
1127         dir.apply_to_path(path=dir.path,
1128                           restore_numeric_ids=restore_numeric_ids)