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