]> arthur.barton.de Git - bup.git/blob - lib/bup/metadata.py
83f04f771475457641c96e369d500b45be946a0b
[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 == errno.EPERM or e.errno == 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 == errno.EPERM \
699                             or e.errno == errno.EOPNOTSUPP:
700                         raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
701                     else:
702                         raise
703             existing_xattrs -= frozenset([k])
704         for k in existing_xattrs:
705             try:
706                 xattr.remove(path, k, nofollow=True)
707             except IOError as e:
708                 if e.errno in (errno.EPERM, errno.EACCES):
709                     raise ApplyError('xattr.remove %r: %s' % (path_msg(path), e))
710                 else:
711                     raise
712
713     def __init__(self):
714         self.mode = self.uid = self.gid = self.user = self.group = None
715         self.atime = self.mtime = self.ctime = None
716         # optional members
717         self.path = None
718         self.size = None
719         self.symlink_target = None
720         self.hardlink_target = None
721         self.linux_attr = None
722         self.linux_xattr = None
723         self.posix1e_acl = None
724
725     def __eq__(self, other):
726         if not isinstance(other, Metadata): return False
727         if self.mode != other.mode: return False
728         if self.mtime != other.mtime: return False
729         if self.ctime != other.ctime: return False
730         if self.atime != other.atime: return False
731         if self.path != other.path: return False
732         if self.uid != other.uid: return False
733         if self.gid != other.gid: return False
734         if self.size != other.size: return False
735         if self.user != other.user: return False
736         if self.group != other.group: return False
737         if self.symlink_target != other.symlink_target: return False
738         if self.hardlink_target != other.hardlink_target: return False
739         if self.linux_attr != other.linux_attr: return False
740         if self.posix1e_acl != other.posix1e_acl: return False
741         return True
742
743     def __ne__(self, other):
744         return not self.__eq__(other)
745
746     def __hash__(self):
747         return hash((self.mode,
748                      self.mtime,
749                      self.ctime,
750                      self.atime,
751                      self.path,
752                      self.uid,
753                      self.gid,
754                      self.size,
755                      self.user,
756                      self.group,
757                      self.symlink_target,
758                      self.hardlink_target,
759                      self.linux_attr,
760                      self.posix1e_acl))
761
762     def __repr__(self):
763         result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
764         if self.path is not None:
765             result += ' path:' + repr(self.path)
766         if self.mode is not None:
767             result += ' mode: %o (%s)' % (self.mode, xstat.mode_str(self.mode))
768         if self.uid is not None:
769             result += ' uid:' + str(self.uid)
770         if self.gid is not None:
771             result += ' gid:' + str(self.gid)
772         if self.user is not None:
773             result += ' user:' + repr(self.user)
774         if self.group is not None:
775             result += ' group:' + repr(self.group)
776         if self.size is not None:
777             result += ' size:' + repr(self.size)
778         for name, val in (('atime', self.atime),
779                           ('mtime', self.mtime),
780                           ('ctime', self.ctime)):
781             if val is not None:
782                 result += ' %s:%r (%d)' \
783                           % (name,
784                              strftime('%Y-%m-%d %H:%M %z',
785                                       gmtime(xstat.fstime_floor_secs(val))),
786                              val)
787         result += '>'
788         return ''.join(result)
789
790     def write(self, port, include_path=True):
791         port.write(self.encode(include_path=include_path))
792
793     def encode(self, include_path=True):
794         ret = []
795         records = include_path and [(_rec_tag_path, self._encode_path())] or []
796         records.extend([(_rec_tag_common_v3, self._encode_common()),
797                         (_rec_tag_symlink_target,
798                          self._encode_symlink_target()),
799                         (_rec_tag_hardlink_target,
800                          self._encode_hardlink_target()),
801                         (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
802                         (_rec_tag_linux_attr, self._encode_linux_attr()),
803                         (_rec_tag_linux_xattr, self._encode_linux_xattr())])
804         for tag, data in records:
805             if data:
806                 ret.extend((vint.encode_vuint(tag),
807                             vint.encode_bvec(data)))
808         ret.append(vint.encode_vuint(_rec_tag_end))
809         return b''.join(ret)
810
811     def copy(self):
812         return deepcopy(self)
813
814     @staticmethod
815     def read(port):
816         # This method should either return a valid Metadata object,
817         # return None if there was no information at all (just a
818         # _rec_tag_end), throw EOFError if there was nothing at all to
819         # read, or throw an Exception if a valid object could not be
820         # read completely.
821         tag = vint.read_vuint(port)
822         if tag == _rec_tag_end:
823             return None
824         try: # From here on, EOF is an error.
825             result = Metadata()
826             while True: # only exit is error (exception) or _rec_tag_end
827                 if tag == _rec_tag_path:
828                     result._load_path_rec(port)
829                 elif tag == _rec_tag_common_v3:
830                     result._load_common_rec(port, version=3)
831                 elif tag == _rec_tag_common_v2:
832                     result._load_common_rec(port, version=2)
833                 elif tag == _rec_tag_symlink_target:
834                     result._load_symlink_target_rec(port)
835                 elif tag == _rec_tag_hardlink_target:
836                     result._load_hardlink_target_rec(port)
837                 elif tag == _rec_tag_posix1e_acl:
838                     result._load_posix1e_acl_rec(port)
839                 elif tag == _rec_tag_linux_attr:
840                     result._load_linux_attr_rec(port)
841                 elif tag == _rec_tag_linux_xattr:
842                     result._load_linux_xattr_rec(port)
843                 elif tag == _rec_tag_end:
844                     return result
845                 elif tag == _rec_tag_common_v1: # Should be very rare.
846                     result._load_common_rec(port, version=1)
847                 else: # unknown record
848                     vint.skip_bvec(port)
849                 tag = vint.read_vuint(port)
850         except EOFError:
851             raise Exception("EOF while reading Metadata")
852
853     def isdir(self):
854         return stat.S_ISDIR(self.mode)
855
856     def create_path(self, path, create_symlinks=True):
857         self._create_via_common_rec(path, create_symlinks=create_symlinks)
858
859     def apply_to_path(self, path=None, restore_numeric_ids=False):
860         # apply metadata to path -- file must exist
861         if not path:
862             path = self.path
863         if not path:
864             raise Exception('Metadata.apply_to_path() called with no path')
865         if not self._recognized_file_type():
866             add_error('not applying metadata to "%s"' % path_msg(path)
867                       + ' with unrecognized mode "0x%x"\n' % self.mode)
868             return
869         num_ids = restore_numeric_ids
870         for apply_metadata in (self._apply_common_rec,
871                                self._apply_posix1e_acl_rec,
872                                self._apply_linux_attr_rec,
873                                self._apply_linux_xattr_rec):
874             try:
875                 apply_metadata(path, restore_numeric_ids=num_ids)
876             except ApplyError as e:
877                 add_error(e)
878
879     def same_file(self, other):
880         """Compare this to other for equivalency.  Return true if
881         their information implies they could represent the same file
882         on disk, in the hardlink sense.  Assume they're both regular
883         files."""
884         return self._same_common(other) \
885             and self._same_hardlink_target(other) \
886             and self._same_posix1e_acl(other) \
887             and self._same_linux_attr(other) \
888             and self._same_linux_xattr(other)
889
890
891 def from_path(path, statinfo=None, archive_path=None,
892               save_symlinks=True, hardlink_target=None,
893               normalized=False, after_stat=None):
894     # This function is also a test hook; see test-save-errors
895     """Return the metadata associated with the path.  When normalized is
896     true, return the metadata appropriate for a typical save, which
897     may or may not be all of it."""
898     result = Metadata()
899     result.path = archive_path
900     st = statinfo or xstat.lstat(path)
901     if after_stat:
902         after_stat(path)
903     result._add_common(path, st)
904     if save_symlinks:
905         result._add_symlink_target(path, st)
906     result._add_hardlink_target(hardlink_target)
907     result._add_posix1e_acl(path, st)
908     result._add_linux_attr(path, st)
909     result._add_linux_xattr(path, st)
910     if normalized:
911         # Only store sizes for regular files and symlinks for now.
912         if not (stat.S_ISREG(result.mode) or stat.S_ISLNK(result.mode)):
913             result.size = None
914     return result
915
916
917 def save_tree(output_file, paths,
918               recurse=False,
919               write_paths=True,
920               save_symlinks=True,
921               xdev=False):
922
923     # Issue top-level rewrite warnings.
924     for path in paths:
925         safe_path = _clean_up_path_for_archive(path)
926         if safe_path != path:
927             log('archiving "%s" as "%s"\n'
928                 % (path_msg(path), path_msg(safe_path)))
929
930     if not recurse:
931         for p in paths:
932             safe_path = _clean_up_path_for_archive(p)
933             st = xstat.lstat(p)
934             if stat.S_ISDIR(st.st_mode):
935                 safe_path += b'/'
936             m = from_path(p, statinfo=st, archive_path=safe_path,
937                           save_symlinks=save_symlinks)
938             if verbose:
939                 print(m.path, file=sys.stderr)
940             m.write(output_file, include_path=write_paths)
941     else:
942         start_dir = os.getcwd()
943         try:
944             for (p, st) in recursive_dirlist(paths, xdev=xdev):
945                 dirlist_dir = os.getcwd()
946                 os.chdir(start_dir)
947                 safe_path = _clean_up_path_for_archive(p)
948                 m = from_path(p, statinfo=st, archive_path=safe_path,
949                               save_symlinks=save_symlinks)
950                 if verbose:
951                     print(m.path, file=sys.stderr)
952                 m.write(output_file, include_path=write_paths)
953                 os.chdir(dirlist_dir)
954         finally:
955             os.chdir(start_dir)
956
957
958 def _set_up_path(meta, create_symlinks=True):
959     # Allow directories to exist as a special case -- might have
960     # been created by an earlier longer path.
961     if meta.isdir():
962         mkdirp(meta.path)
963     else:
964         parent = os.path.dirname(meta.path)
965         if parent:
966             mkdirp(parent)
967         meta.create_path(meta.path, create_symlinks=create_symlinks)
968
969
970 all_fields = frozenset(['path',
971                         'mode',
972                         'link-target',
973                         'rdev',
974                         'size',
975                         'uid',
976                         'gid',
977                         'user',
978                         'group',
979                         'atime',
980                         'mtime',
981                         'ctime',
982                         'linux-attr',
983                         'linux-xattr',
984                         'posix1e-acl'])
985
986
987 def summary_bytes(meta, numeric_ids = False, classification = None,
988                   human_readable = False):
989     """Return bytes containing the "ls -l" style listing for meta.
990     Classification may be "all", "type", or None."""
991     user_str = group_str = size_or_dev_str = b'?'
992     symlink_target = None
993     if meta:
994         name = meta.path
995         mode_str = xstat.mode_str(meta.mode).encode('ascii')
996         symlink_target = meta.symlink_target
997         mtime_secs = xstat.fstime_floor_secs(meta.mtime)
998         mtime_str = strftime('%Y-%m-%d %H:%M',
999                              time.localtime(mtime_secs)).encode('ascii')
1000         if meta.user and not numeric_ids:
1001             user_str = meta.user
1002         elif meta.uid != None:
1003             user_str = str(meta.uid).encode()
1004         if meta.group and not numeric_ids:
1005             group_str = meta.group
1006         elif meta.gid != None:
1007             group_str = str(meta.gid).encode()
1008         if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
1009             if meta.rdev:
1010                 size_or_dev_str = ('%d,%d' % (os.major(meta.rdev),
1011                                               os.minor(meta.rdev))).encode()
1012         elif meta.size != None:
1013             if human_readable:
1014                 size_or_dev_str = format_filesize(meta.size).encode()
1015             else:
1016                 size_or_dev_str = str(meta.size).encode()
1017         else:
1018             size_or_dev_str = b'-'
1019         if classification:
1020             classification_str = \
1021                 xstat.classification_str(meta.mode,
1022                                          classification == 'all').encode()
1023     else:
1024         mode_str = b'?' * 10
1025         mtime_str = b'????-??-?? ??:??'
1026         classification_str = b'?'
1027
1028     name = name or b''
1029     if classification:
1030         name += classification_str
1031     if symlink_target:
1032         name += b' -> ' + meta.symlink_target
1033
1034     return b'%-10s %-11s %11s %16s %s' % (mode_str,
1035                                           user_str + b'/' + group_str,
1036                                           size_or_dev_str,
1037                                           mtime_str,
1038                                           name)
1039
1040
1041 def detailed_bytes(meta, fields = None):
1042     # FIXME: should optional fields be omitted, or empty i.e. "rdev:
1043     # 0", "link-target:", etc.
1044     if not fields:
1045         fields = all_fields
1046
1047     result = []
1048     if 'path' in fields:
1049         path = meta.path or b''
1050         result.append(b'path: ' + path)
1051     if 'mode' in fields:
1052         result.append(b'mode: %o (%s)'
1053                       % (meta.mode, xstat.mode_str(meta.mode).encode('ascii')))
1054     if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1055         result.append(b'link-target: ' + meta.symlink_target)
1056     if 'rdev' in fields:
1057         if meta.rdev:
1058             result.append(b'rdev: %d,%d' % (os.major(meta.rdev),
1059                                             os.minor(meta.rdev)))
1060         else:
1061             result.append(b'rdev: 0')
1062     if 'size' in fields and meta.size is not None:
1063         result.append(b'size: %d' % meta.size)
1064     if 'uid' in fields:
1065         result.append(b'uid: %d' % meta.uid)
1066     if 'gid' in fields:
1067         result.append(b'gid: %d' % meta.gid)
1068     if 'user' in fields:
1069         result.append(b'user: ' + meta.user)
1070     if 'group' in fields:
1071         result.append(b'group: ' + meta.group)
1072     if 'atime' in fields:
1073         # If we don't have xstat.lutime, that means we have to use
1074         # utime(), and utime() has no way to set the mtime/atime of a
1075         # symlink.  Thus, the mtime/atime of a symlink is meaningless,
1076         # so let's not report it.  (That way scripts comparing
1077         # before/after won't trigger.)
1078         if xstat.lutime or not stat.S_ISLNK(meta.mode):
1079             result.append(b'atime: ' + xstat.fstime_to_sec_bytes(meta.atime))
1080         else:
1081             result.append(b'atime: 0')
1082     if 'mtime' in fields:
1083         if xstat.lutime or not stat.S_ISLNK(meta.mode):
1084             result.append(b'mtime: ' + xstat.fstime_to_sec_bytes(meta.mtime))
1085         else:
1086             result.append(b'mtime: 0')
1087     if 'ctime' in fields:
1088         result.append(b'ctime: ' + xstat.fstime_to_sec_bytes(meta.ctime))
1089     if 'linux-attr' in fields and meta.linux_attr:
1090         result.append(b'linux-attr: %x' % meta.linux_attr)
1091     if 'linux-xattr' in fields and meta.linux_xattr:
1092         for name, value in meta.linux_xattr:
1093             result.append(b'linux-xattr: %s -> %s' % (name, value))
1094     if 'posix1e-acl' in fields and meta.posix1e_acl:
1095         acl = meta.posix1e_acl[0]
1096         result.append(b'posix1e-acl: ' + acl + b'\n')
1097         if stat.S_ISDIR(meta.mode):
1098             def_acl = meta.posix1e_acl[2]
1099             result.append(b'posix1e-acl-default: ' + def_acl + b'\n')
1100     return b'\n'.join(result)
1101
1102
1103 class _ArchiveIterator:
1104     def __next__(self):
1105         try:
1106             return Metadata.read(self._file)
1107         except EOFError:
1108             raise StopIteration()
1109
1110     next = __next__
1111
1112     def __iter__(self):
1113         return self
1114
1115     def __init__(self, file):
1116         self._file = file
1117
1118
1119 def display_archive(file, out):
1120     if verbose > 1:
1121         first_item = True
1122         for meta in _ArchiveIterator(file):
1123             if not first_item:
1124                 out.write(b'\n')
1125             out.write(detailed_bytes(meta))
1126             out.write(b'\n')
1127             first_item = False
1128     elif verbose > 0:
1129         for meta in _ArchiveIterator(file):
1130             out.write(summary_bytes(meta))
1131             out.write(b'\n')
1132     elif verbose == 0:
1133         for meta in _ArchiveIterator(file):
1134             if not meta.path:
1135                 log('bup: no metadata path, but asked to only display path'
1136                     ' (increase verbosity?)')
1137                 sys.exit(1)
1138             out.write(meta.path)
1139             out.write(b'\n')
1140
1141
1142 def start_extract(file, create_symlinks=True):
1143     for meta in _ArchiveIterator(file):
1144         if not meta: # Hit end record.
1145             break
1146         if verbose:
1147             print(path_msg(meta.path), file=sys.stderr)
1148         xpath = _clean_up_extract_path(meta.path)
1149         if not xpath:
1150             add_error(Exception('skipping risky path "%s"'
1151                                 % path_msg(meta.path)))
1152         else:
1153             meta.path = xpath
1154             _set_up_path(meta, create_symlinks=create_symlinks)
1155
1156
1157 def finish_extract(file, restore_numeric_ids=False):
1158     all_dirs = []
1159     for meta in _ArchiveIterator(file):
1160         if not meta: # Hit end record.
1161             break
1162         xpath = _clean_up_extract_path(meta.path)
1163         if not xpath:
1164             add_error(Exception('skipping risky path "%s"'
1165                                 % path_msg(meta.path)))
1166         else:
1167             if os.path.isdir(meta.path):
1168                 all_dirs.append(meta)
1169             else:
1170                 if verbose:
1171                     print(path_msg(meta.path), file=sys.stderr)
1172                 meta.apply_to_path(path=xpath,
1173                                    restore_numeric_ids=restore_numeric_ids)
1174     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1175     for dir in all_dirs:
1176         # Don't need to check xpath -- won't be in all_dirs if not OK.
1177         xpath = _clean_up_extract_path(dir.path)
1178         if verbose:
1179             print(path_msg(dir.path), file=sys.stderr)
1180         dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1181
1182
1183 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1184     # For now, just store all the directories and handle them last,
1185     # longest first.
1186     all_dirs = []
1187     for meta in _ArchiveIterator(file):
1188         if not meta: # Hit end record.
1189             break
1190         xpath = _clean_up_extract_path(meta.path)
1191         if not xpath:
1192             add_error(Exception('skipping risky path "%s"'
1193                                 % path_msg(meta.path)))
1194         else:
1195             meta.path = xpath
1196             if verbose:
1197                 print('+', path_msg(meta.path), file=sys.stderr)
1198             _set_up_path(meta, create_symlinks=create_symlinks)
1199             if os.path.isdir(meta.path):
1200                 all_dirs.append(meta)
1201             else:
1202                 if verbose:
1203                     print('=', path_msg(meta.path), file=sys.stderr)
1204                 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1205     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1206     for dir in all_dirs:
1207         # Don't need to check xpath -- won't be in all_dirs if not OK.
1208         xpath = _clean_up_extract_path(dir.path)
1209         if verbose:
1210             print('=', path_msg(xpath), file=sys.stderr)
1211         # Shouldn't have to check for risky paths here (omitted above).
1212         dir.apply_to_path(path=dir.path,
1213                           restore_numeric_ids=restore_numeric_ids)