]> arthur.barton.de Git - bup.git/blob - lib/bup/metadata.py
test: add pylint and test imports
[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 errno.ENOSYS:  # Function not implemented
457                 pass
458         elif not stat.S_ISLNK(self.mode):
459             os.chmod(path, stat.S_IMODE(self.mode))
460
461
462     ## Path records
463
464     def _encode_path(self):
465         if self.path:
466             return vint.pack('s', self.path)
467         else:
468             return None
469
470     def _load_path_rec(self, port):
471         self.path = vint.unpack('s', vint.read_bvec(port))[0]
472
473
474     ## Symlink targets
475
476     def _add_symlink_target(self, path, st):
477         try:
478             if stat.S_ISLNK(st.st_mode):
479                 self.symlink_target = os.readlink(path)
480                 # might have read a different link than the
481                 # one that was in place when we did stat()
482                 self.size = len(self.symlink_target)
483         except OSError as e:
484             add_error('readlink: %s' % e)
485
486     def _encode_symlink_target(self):
487         return self.symlink_target
488
489     def _load_symlink_target_rec(self, port):
490         target = vint.read_bvec(port)
491         self.symlink_target = target
492         if self.size is None:
493             self.size = len(target)
494         else:
495             assert(self.size == len(target))
496
497
498     ## Hardlink targets
499
500     def _add_hardlink_target(self, target):
501         self.hardlink_target = target
502
503     def _same_hardlink_target(self, other):
504         """Return true or false to indicate similarity in the hardlink sense."""
505         return self.hardlink_target == other.hardlink_target
506
507     def _encode_hardlink_target(self):
508         return self.hardlink_target
509
510     def _load_hardlink_target_rec(self, port):
511         self.hardlink_target = vint.read_bvec(port)
512
513
514     ## POSIX1e ACL records
515
516     # Recorded as a list:
517     #   [txt_id_acl, num_id_acl]
518     # or, if a directory:
519     #   [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
520     # The numeric/text distinction only matters when reading/restoring
521     # a stored record.
522     def _add_posix1e_acl(self, path, st):
523         if not read_acl:
524             return
525         if not stat.S_ISLNK(st.st_mode):
526             isdir = 1 if stat.S_ISDIR(st.st_mode) else 0
527             self.posix1e_acl = read_acl(path, isdir)
528
529     def _same_posix1e_acl(self, other):
530         """Return true or false to indicate similarity in the hardlink sense."""
531         return self.posix1e_acl == other.posix1e_acl
532
533     def _encode_posix1e_acl(self):
534         # Encode as two strings (w/default ACL string possibly empty).
535         if self.posix1e_acl:
536             acls = self.posix1e_acl
537             if len(acls) == 2:
538                 return vint.pack('ssss', acls[0], acls[1], b'', b'')
539             return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
540         else:
541             return None
542
543     def _load_posix1e_acl_rec(self, port):
544         acl_rep = vint.unpack('ssss', vint.read_bvec(port))
545         if acl_rep[2] == b'':
546             acl_rep = acl_rep[:2]
547         self.posix1e_acl = acl_rep
548
549     def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
550         if not self.posix1e_acl:
551             return
552
553         if not apply_acl:
554             add_error("%s: can't restore ACLs; posix1e support missing.\n"
555                       % path_msg(path))
556             return
557
558         try:
559             acls = self.posix1e_acl
560             offs = 1 if restore_numeric_ids else 0
561             if len(acls) > 2:
562                 apply_acl(path, acls[offs], acls[offs + 2])
563             else:
564                 apply_acl(path, acls[offs])
565         except IOError as e:
566             if e.errno == errno.EINVAL:
567                 # libacl returns with errno set to EINVAL if a user
568                 # (or group) doesn't exist
569                 raise ApplyError("POSIX1e ACL: can't create %r for %r"
570                                  % (acls, path_msg(path)))
571             elif e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
572                 raise ApplyError('POSIX1e ACL applyto: %s' % e)
573             else:
574                 raise
575
576
577     ## Linux attributes (lsattr(1), chattr(1))
578
579     def _add_linux_attr(self, path, st):
580         check_linux_file_attr_api()
581         if not get_linux_file_attr: return
582         if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
583             try:
584                 attr = get_linux_file_attr(path)
585                 if attr != 0:
586                     self.linux_attr = attr
587             except OSError as e:
588                 if e.errno == errno.EACCES:
589                     add_error('read Linux attr: %s' % e)
590                 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
591                     # Assume filesystem doesn't support attrs.
592                     return
593                 elif e.errno == EINVAL:
594                     global _warned_about_attr_einval
595                     if not _warned_about_attr_einval:
596                         log("Ignoring attr EINVAL;"
597                             + " if you're not using ntfs-3g, please report: "
598                             + path_msg(path) + '\n')
599                         _warned_about_attr_einval = True
600                     return
601                 else:
602                     raise
603
604     def _same_linux_attr(self, other):
605         """Return true or false to indicate similarity in the hardlink sense."""
606         return self.linux_attr == other.linux_attr
607
608     def _encode_linux_attr(self):
609         if self.linux_attr:
610             return vint.pack('V', self.linux_attr)
611         else:
612             return None
613
614     def _load_linux_attr_rec(self, port):
615         data = vint.read_bvec(port)
616         self.linux_attr = vint.unpack('V', data)[0]
617
618     def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
619         if self.linux_attr:
620             check_linux_file_attr_api()
621             if not set_linux_file_attr:
622                 add_error("%s: can't restore linuxattrs: "
623                           "linuxattr support missing.\n" % path_msg(path))
624                 return
625             try:
626                 set_linux_file_attr(path, self.linux_attr)
627             except OSError as e:
628                 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
629                     raise ApplyError('Linux chattr: %s (0x%s)'
630                                      % (e, hex(self.linux_attr)))
631                 elif e.errno == EINVAL:
632                     msg = "if you're not using ntfs-3g, please report"
633                     raise ApplyError('Linux chattr: %s (0x%s) (%s)'
634                                      % (e, hex(self.linux_attr), msg))
635                 else:
636                     raise
637
638
639     ## Linux extended attributes (getfattr(1), setfattr(1))
640
641     def _add_linux_xattr(self, path, st):
642         if not xattr: return
643         try:
644             self.linux_xattr = xattr.get_all(path, nofollow=True)
645         except EnvironmentError as e:
646             if e.errno != errno.EOPNOTSUPP:
647                 raise
648
649     def _same_linux_xattr(self, other):
650         """Return true or false to indicate similarity in the hardlink sense."""
651         return self.linux_xattr == other.linux_xattr
652
653     def _encode_linux_xattr(self):
654         if self.linux_xattr:
655             result = vint.pack('V', len(self.linux_xattr))
656             for name, value in self.linux_xattr:
657                 result += vint.pack('ss', name, value)
658             return result
659         else:
660             return None
661
662     def _load_linux_xattr_rec(self, file):
663         data = vint.read_bvec(file)
664         memfile = BytesIO(data)
665         result = []
666         for i in range(vint.read_vuint(memfile)):
667             key = vint.read_bvec(memfile)
668             value = vint.read_bvec(memfile)
669             result.append((key, value))
670         self.linux_xattr = result
671
672     def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
673         if not xattr:
674             if self.linux_xattr:
675                 add_error("%s: can't restore xattr; xattr support missing.\n"
676                           % path_msg(path))
677             return
678         if not self.linux_xattr:
679             return
680         try:
681             existing_xattrs = set(xattr.list(path, nofollow=True))
682         except IOError as e:
683             if e.errno == errno.EACCES:
684                 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
685             else:
686                 raise
687         for k, v in self.linux_xattr:
688             if k not in existing_xattrs \
689                     or v != xattr.get(path, k, nofollow=True):
690                 try:
691                     xattr.set(path, k, v, nofollow=True)
692                 except IOError as e:
693                     if e.errno == errno.EPERM \
694                             or e.errno == errno.EOPNOTSUPP:
695                         raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
696                     else:
697                         raise
698             existing_xattrs -= frozenset([k])
699         for k in existing_xattrs:
700             try:
701                 xattr.remove(path, k, nofollow=True)
702             except IOError as e:
703                 if e.errno in (errno.EPERM, errno.EACCES):
704                     raise ApplyError('xattr.remove %r: %s' % (path_msg(path), e))
705                 else:
706                     raise
707
708     def __init__(self):
709         self.mode = self.uid = self.gid = self.user = self.group = None
710         self.atime = self.mtime = self.ctime = None
711         # optional members
712         self.path = None
713         self.size = None
714         self.symlink_target = None
715         self.hardlink_target = None
716         self.linux_attr = None
717         self.linux_xattr = None
718         self.posix1e_acl = None
719
720     def __eq__(self, other):
721         if not isinstance(other, Metadata): return False
722         if self.mode != other.mode: return False
723         if self.mtime != other.mtime: return False
724         if self.ctime != other.ctime: return False
725         if self.atime != other.atime: return False
726         if self.path != other.path: return False
727         if self.uid != other.uid: return False
728         if self.gid != other.gid: return False
729         if self.size != other.size: return False
730         if self.user != other.user: return False
731         if self.group != other.group: return False
732         if self.symlink_target != other.symlink_target: return False
733         if self.hardlink_target != other.hardlink_target: return False
734         if self.linux_attr != other.linux_attr: return False
735         if self.posix1e_acl != other.posix1e_acl: return False
736         return True
737
738     def __ne__(self, other):
739         return not self.__eq__(other)
740
741     def __hash__(self):
742         return hash((self.mode,
743                      self.mtime,
744                      self.ctime,
745                      self.atime,
746                      self.path,
747                      self.uid,
748                      self.gid,
749                      self.size,
750                      self.user,
751                      self.group,
752                      self.symlink_target,
753                      self.hardlink_target,
754                      self.linux_attr,
755                      self.posix1e_acl))
756
757     def __repr__(self):
758         result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
759         if self.path is not None:
760             result += ' path:' + repr(self.path)
761         if self.mode is not None:
762             result += ' mode: %o (%s)' % (self.mode, xstat.mode_str(self.mode))
763         if self.uid is not None:
764             result += ' uid:' + str(self.uid)
765         if self.gid is not None:
766             result += ' gid:' + str(self.gid)
767         if self.user is not None:
768             result += ' user:' + repr(self.user)
769         if self.group is not None:
770             result += ' group:' + repr(self.group)
771         if self.size is not None:
772             result += ' size:' + repr(self.size)
773         for name, val in (('atime', self.atime),
774                           ('mtime', self.mtime),
775                           ('ctime', self.ctime)):
776             if val is not None:
777                 result += ' %s:%r (%d)' \
778                           % (name,
779                              strftime('%Y-%m-%d %H:%M %z',
780                                       gmtime(xstat.fstime_floor_secs(val))),
781                              val)
782         result += '>'
783         return ''.join(result)
784
785     def write(self, port, include_path=True):
786         port.write(self.encode(include_path=include_path))
787
788     def encode(self, include_path=True):
789         ret = []
790         records = include_path and [(_rec_tag_path, self._encode_path())] or []
791         records.extend([(_rec_tag_common_v3, self._encode_common()),
792                         (_rec_tag_symlink_target,
793                          self._encode_symlink_target()),
794                         (_rec_tag_hardlink_target,
795                          self._encode_hardlink_target()),
796                         (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
797                         (_rec_tag_linux_attr, self._encode_linux_attr()),
798                         (_rec_tag_linux_xattr, self._encode_linux_xattr())])
799         for tag, data in records:
800             if data:
801                 ret.extend((vint.encode_vuint(tag),
802                             vint.encode_bvec(data)))
803         ret.append(vint.encode_vuint(_rec_tag_end))
804         return b''.join(ret)
805
806     def copy(self):
807         return deepcopy(self)
808
809     @staticmethod
810     def read(port):
811         # This method should either return a valid Metadata object,
812         # return None if there was no information at all (just a
813         # _rec_tag_end), throw EOFError if there was nothing at all to
814         # read, or throw an Exception if a valid object could not be
815         # read completely.
816         tag = vint.read_vuint(port)
817         if tag == _rec_tag_end:
818             return None
819         try: # From here on, EOF is an error.
820             result = Metadata()
821             while True: # only exit is error (exception) or _rec_tag_end
822                 if tag == _rec_tag_path:
823                     result._load_path_rec(port)
824                 elif tag == _rec_tag_common_v3:
825                     result._load_common_rec(port, version=3)
826                 elif tag == _rec_tag_common_v2:
827                     result._load_common_rec(port, version=2)
828                 elif tag == _rec_tag_symlink_target:
829                     result._load_symlink_target_rec(port)
830                 elif tag == _rec_tag_hardlink_target:
831                     result._load_hardlink_target_rec(port)
832                 elif tag == _rec_tag_posix1e_acl:
833                     result._load_posix1e_acl_rec(port)
834                 elif tag == _rec_tag_linux_attr:
835                     result._load_linux_attr_rec(port)
836                 elif tag == _rec_tag_linux_xattr:
837                     result._load_linux_xattr_rec(port)
838                 elif tag == _rec_tag_end:
839                     return result
840                 elif tag == _rec_tag_common_v1: # Should be very rare.
841                     result._load_common_rec(port, version=1)
842                 else: # unknown record
843                     vint.skip_bvec(port)
844                 tag = vint.read_vuint(port)
845         except EOFError:
846             raise Exception("EOF while reading Metadata")
847
848     def isdir(self):
849         return stat.S_ISDIR(self.mode)
850
851     def create_path(self, path, create_symlinks=True):
852         self._create_via_common_rec(path, create_symlinks=create_symlinks)
853
854     def apply_to_path(self, path=None, restore_numeric_ids=False):
855         # apply metadata to path -- file must exist
856         if not path:
857             path = self.path
858         if not path:
859             raise Exception('Metadata.apply_to_path() called with no path')
860         if not self._recognized_file_type():
861             add_error('not applying metadata to "%s"' % path_msg(path)
862                       + ' with unrecognized mode "0x%x"\n' % self.mode)
863             return
864         num_ids = restore_numeric_ids
865         for apply_metadata in (self._apply_common_rec,
866                                self._apply_posix1e_acl_rec,
867                                self._apply_linux_attr_rec,
868                                self._apply_linux_xattr_rec):
869             try:
870                 apply_metadata(path, restore_numeric_ids=num_ids)
871             except ApplyError as e:
872                 add_error(e)
873
874     def same_file(self, other):
875         """Compare this to other for equivalency.  Return true if
876         their information implies they could represent the same file
877         on disk, in the hardlink sense.  Assume they're both regular
878         files."""
879         return self._same_common(other) \
880             and self._same_hardlink_target(other) \
881             and self._same_posix1e_acl(other) \
882             and self._same_linux_attr(other) \
883             and self._same_linux_xattr(other)
884
885
886 def from_path(path, statinfo=None, archive_path=None,
887               save_symlinks=True, hardlink_target=None,
888               normalized=False, after_stat=None):
889     # This function is also a test hook; see test-save-errors
890     """Return the metadata associated with the path.  When normalized is
891     true, return the metadata appropriate for a typical save, which
892     may or may not be all of it."""
893     result = Metadata()
894     result.path = archive_path
895     st = statinfo or xstat.lstat(path)
896     if after_stat:
897         after_stat(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)