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