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