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