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