]> arthur.barton.de Git - bup.git/blob - lib/bup/metadata.py
metadata: use python-xattr pyxattr compat code
[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, pwd, grp, 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.pwdgrp import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
19 from bup.xstat import utime, lutime
20
21 xattr = None
22 if sys.platform.startswith('linux'):
23     # prefer python-pyxattr (it's a lot faster), but fall back to python-xattr
24     # as the two are incompatible and only one can be installed on a system
25     try:
26         import xattr
27     except ImportError:
28         log('Warning: Linux xattr support missing; install python-pyxattr.\n')
29     if xattr and getattr(xattr, 'get_all', None) is None:
30         try:
31             from xattr import pyxattr_compat as xattr
32         except ImportError:
33             log('Warning: python-xattr module is too old; '
34                 'upgrade or install python-pyxattr instead.\n')
35             xattr = None
36
37 posix1e = None
38 if not (sys.platform.startswith('cygwin') \
39         or sys.platform.startswith('darwin') \
40         or sys.platform.startswith('netbsd')):
41     try:
42         import posix1e
43     except ImportError:
44         log('Warning: POSIX ACL support missing; install python-pylibacl.\n')
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('/../')
126     if pos != -1:
127         result = result[result.rfind('/../') + 4:]
128
129     # Take everything after any remaining '../'.
130     if result.startswith("../"):
131         result = result[3:]
132
133     # Remove any '/./' sequences.
134     pos = result.find('/./')
135     while pos != -1:
136         result = result[0:pos] + '/' + result[pos + 3:]
137         pos = result.find('/./')
138
139     # Remove any leading '/'s.
140     result = result.lstrip('/')
141
142     # Replace '//' with '/' everywhere.
143     pos = result.find('//')
144     while pos != -1:
145         result = result[0:pos] + '/' + result[pos + 2:]
146         pos = result.find('//')
147
148     # Take everything after any remaining './'.
149     if result.startswith('./'):
150         result = result[2:]
151
152     # Take everything before any remaining '/.'.
153     if result.endswith('/.'):
154         result = result[:-2]
155
156     if result == '' or result.endswith('/..'):
157         result = '.'
158
159     return result
160
161
162 def _risky_path(p):
163     if p.startswith('/'):
164         return True
165     if p.find('/../') != -1:
166         return True
167     if p.startswith('../'):
168         return True
169     if p.endswith('/..'):
170         return True
171     return False
172
173
174 def _clean_up_extract_path(p):
175     result = p.lstrip('/')
176     if result == '':
177         return '.'
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 = ''
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 ' + path)
326
327         # If the path already exists and is a dir, try rmdir.
328         # If the path already exists and is anything else, try unlink.
329         st = None
330         try:
331             st = xstat.lstat(path)
332         except OSError as e:
333             if e.errno != errno.ENOENT:
334                 raise
335         if st:
336             if stat.S_ISDIR(st.st_mode):
337                 try:
338                     os.rmdir(path)
339                 except OSError as e:
340                     if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
341                         msg = 'refusing to overwrite non-empty dir ' + path
342                         raise Exception(msg)
343                     raise
344             else:
345                 os.unlink(path)
346
347         if stat.S_ISREG(self.mode):
348             assert(self._recognized_file_type())
349             fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
350             os.close(fd)
351         elif stat.S_ISDIR(self.mode):
352             assert(self._recognized_file_type())
353             os.mkdir(path, 0o700)
354         elif stat.S_ISCHR(self.mode):
355             assert(self._recognized_file_type())
356             os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
357         elif stat.S_ISBLK(self.mode):
358             assert(self._recognized_file_type())
359             os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
360         elif stat.S_ISFIFO(self.mode):
361             assert(self._recognized_file_type())
362             os.mkfifo(path, 0o600 | stat.S_IFIFO)
363         elif stat.S_ISSOCK(self.mode):
364             try:
365                 os.mknod(path, 0o600 | stat.S_IFSOCK)
366             except OSError as e:
367                 if e.errno in (errno.EINVAL, errno.EPERM):
368                     s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
369                     s.bind(path)
370                 else:
371                     raise
372         elif stat.S_ISLNK(self.mode):
373             assert(self._recognized_file_type())
374             if self.symlink_target and create_symlinks:
375                 # on MacOS, symlink() permissions depend on umask, and there's
376                 # no way to chown a symlink after creating it, so we have to
377                 # be careful here!
378                 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
379                 try:
380                     os.symlink(self.symlink_target, path)
381                 finally:
382                     os.umask(oldumask)
383         # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
384         else:
385             assert(not self._recognized_file_type())
386             add_error('not creating "%s" with unrecognized mode "0x%x"\n'
387                       % (path, self.mode))
388
389     def _apply_common_rec(self, path, restore_numeric_ids=False):
390         if not self.mode:
391             raise ApplyError('no metadata - cannot apply to ' + path)
392
393         # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
394         # EACCES errors at this stage are fatal for the current path.
395         if lutime and stat.S_ISLNK(self.mode):
396             try:
397                 lutime(path, (self.atime, self.mtime))
398             except OSError as e:
399                 if e.errno == errno.EACCES:
400                     raise ApplyError('lutime: %s' % e)
401                 else:
402                     raise
403         else:
404             try:
405                 utime(path, (self.atime, self.mtime))
406             except OSError as e:
407                 if e.errno == errno.EACCES:
408                     raise ApplyError('utime: %s' % e)
409                 else:
410                     raise
411
412         uid = gid = -1 # By default, do nothing.
413         if is_superuser():
414             uid = self.uid
415             gid = self.gid
416             if not restore_numeric_ids:
417                 if self.uid != 0 and self.user:
418                     entry = pwd_from_name(self.user)
419                     if entry:
420                         uid = entry.pw_uid
421                 if self.gid != 0 and self.group:
422                     entry = grp_from_name(self.group)
423                     if entry:
424                         gid = entry.gr_gid
425         else: # not superuser - only consider changing the group/gid
426             user_gids = os.getgroups()
427             if self.gid in user_gids:
428                 gid = self.gid
429             if not restore_numeric_ids and self.gid != 0:
430                 # The grp might not exist on the local system.
431                 grps = filter(None, [grp_from_gid(x) for x in user_gids])
432                 if self.group in [x.gr_name for x in grps]:
433                     g = grp_from_name(self.group)
434                     if g:
435                         gid = g.gr_gid
436
437         if uid != -1 or gid != -1:
438             try:
439                 os.lchown(path, uid, gid)
440             except OSError as e:
441                 if e.errno == errno.EPERM:
442                     add_error('lchown: %s' %  e)
443                 elif sys.platform.startswith('cygwin') \
444                    and e.errno == errno.EINVAL:
445                     add_error('lchown: unknown uid/gid (%d/%d) for %s'
446                               %  (uid, gid, path))
447                 else:
448                     raise
449
450         if _have_lchmod:
451             try:
452                 os.lchmod(path, stat.S_IMODE(self.mode))
453             except errno.ENOSYS:  # Function not implemented
454                 pass
455         elif not stat.S_ISLNK(self.mode):
456             os.chmod(path, stat.S_IMODE(self.mode))
457
458
459     ## Path records
460
461     def _encode_path(self):
462         if self.path:
463             return vint.pack('s', self.path)
464         else:
465             return None
466
467     def _load_path_rec(self, port):
468         self.path = vint.unpack('s', vint.read_bvec(port))[0]
469
470
471     ## Symlink targets
472
473     def _add_symlink_target(self, path, st):
474         try:
475             if stat.S_ISLNK(st.st_mode):
476                 self.symlink_target = os.readlink(path)
477         except OSError as e:
478             add_error('readlink: %s' % e)
479
480     def _encode_symlink_target(self):
481         return self.symlink_target
482
483     def _load_symlink_target_rec(self, port):
484         target = vint.read_bvec(port)
485         self.symlink_target = target
486         if self.size is None:
487             self.size = len(target)
488         else:
489             assert(self.size == len(target))
490
491
492     ## Hardlink targets
493
494     def _add_hardlink_target(self, target):
495         self.hardlink_target = target
496
497     def _same_hardlink_target(self, other):
498         """Return true or false to indicate similarity in the hardlink sense."""
499         return self.hardlink_target == other.hardlink_target
500
501     def _encode_hardlink_target(self):
502         return self.hardlink_target
503
504     def _load_hardlink_target_rec(self, port):
505         self.hardlink_target = vint.read_bvec(port)
506
507
508     ## POSIX1e ACL records
509
510     # Recorded as a list:
511     #   [txt_id_acl, num_id_acl]
512     # or, if a directory:
513     #   [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
514     # The numeric/text distinction only matters when reading/restoring
515     # a stored record.
516     def _add_posix1e_acl(self, path, st):
517         if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
518             return
519         if not stat.S_ISLNK(st.st_mode):
520             acls = None
521             def_acls = None
522             try:
523                 if posix1e.has_extended(path):
524                     acl = posix1e.ACL(file=path)
525                     acls = [acl, acl] # txt and num are the same
526                     if stat.S_ISDIR(st.st_mode):
527                         def_acl = posix1e.ACL(filedef=path)
528                         def_acls = [def_acl, def_acl]
529             except EnvironmentError as e:
530                 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
531                     raise
532             if acls:
533                 txt_flags = posix1e.TEXT_ABBREVIATE
534                 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
535                 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
536                            acls[1].to_any_text('', '\n', num_flags)]
537                 if def_acls:
538                     acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
539                     acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
540                 self.posix1e_acl = acl_rep
541
542     def _same_posix1e_acl(self, other):
543         """Return true or false to indicate similarity in the hardlink sense."""
544         return self.posix1e_acl == other.posix1e_acl
545
546     def _encode_posix1e_acl(self):
547         # Encode as two strings (w/default ACL string possibly empty).
548         if self.posix1e_acl:
549             acls = self.posix1e_acl
550             if len(acls) == 2:
551                 acls.extend(['', ''])
552             return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
553         else:
554             return None
555
556     def _load_posix1e_acl_rec(self, port):
557         acl_rep = vint.unpack('ssss', vint.read_bvec(port))
558         if acl_rep[2] == '':
559             acl_rep = acl_rep[:2]
560         self.posix1e_acl = acl_rep
561
562     def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
563         def apply_acl(acl_rep, kind):
564             try:
565                 acl = posix1e.ACL(text = acl_rep)
566             except IOError as e:
567                 if e.errno == 0:
568                     # pylibacl appears to return an IOError with errno
569                     # set to 0 if a group referred to by the ACL rep
570                     # doesn't exist on the current system.
571                     raise ApplyError("POSIX1e ACL: can't create %r for %r"
572                                      % (acl_rep, path))
573                 else:
574                     raise
575             try:
576                 acl.applyto(path, kind)
577             except IOError as e:
578                 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
579                     raise ApplyError('POSIX1e ACL applyto: %s' % e)
580                 else:
581                     raise
582
583         if not posix1e:
584             if self.posix1e_acl:
585                 add_error("%s: can't restore ACLs; posix1e support missing.\n"
586                           % path)
587             return
588         if self.posix1e_acl:
589             acls = self.posix1e_acl
590             if len(acls) > 2:
591                 if restore_numeric_ids:
592                     apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
593                 else:
594                     apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
595             if restore_numeric_ids:
596                 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
597             else:
598                 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
599
600
601     ## Linux attributes (lsattr(1), chattr(1))
602
603     def _add_linux_attr(self, path, st):
604         check_linux_file_attr_api()
605         if not get_linux_file_attr: return
606         if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
607             try:
608                 attr = get_linux_file_attr(path)
609                 if attr != 0:
610                     self.linux_attr = attr
611             except OSError as e:
612                 if e.errno == errno.EACCES:
613                     add_error('read Linux attr: %s' % e)
614                 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
615                     # Assume filesystem doesn't support attrs.
616                     return
617                 elif e.errno == EINVAL:
618                     global _warned_about_attr_einval
619                     if not _warned_about_attr_einval:
620                         log("Ignoring attr EINVAL;"
621                             + " if you're not using ntfs-3g, please report: "
622                             + repr(path) + '\n')
623                         _warned_about_attr_einval = True
624                     return
625                 else:
626                     raise
627
628     def _same_linux_attr(self, other):
629         """Return true or false to indicate similarity in the hardlink sense."""
630         return self.linux_attr == other.linux_attr
631
632     def _encode_linux_attr(self):
633         if self.linux_attr:
634             return vint.pack('V', self.linux_attr)
635         else:
636             return None
637
638     def _load_linux_attr_rec(self, port):
639         data = vint.read_bvec(port)
640         self.linux_attr = vint.unpack('V', data)[0]
641
642     def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
643         if self.linux_attr:
644             check_linux_file_attr_api()
645             if not set_linux_file_attr:
646                 add_error("%s: can't restore linuxattrs: "
647                           "linuxattr support missing.\n" % path)
648                 return
649             try:
650                 set_linux_file_attr(path, self.linux_attr)
651             except OSError as e:
652                 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
653                     raise ApplyError('Linux chattr: %s (0x%s)'
654                                      % (e, hex(self.linux_attr)))
655                 elif e.errno == EINVAL:
656                     msg = "if you're not using ntfs-3g, please report"
657                     raise ApplyError('Linux chattr: %s (0x%s) (%s)'
658                                      % (e, hex(self.linux_attr), msg))
659                 else:
660                     raise
661
662
663     ## Linux extended attributes (getfattr(1), setfattr(1))
664
665     def _add_linux_xattr(self, path, st):
666         if not xattr: return
667         try:
668             self.linux_xattr = xattr.get_all(path, nofollow=True)
669         except EnvironmentError as e:
670             if e.errno != errno.EOPNOTSUPP:
671                 raise
672
673     def _same_linux_xattr(self, other):
674         """Return true or false to indicate similarity in the hardlink sense."""
675         return self.linux_xattr == other.linux_xattr
676
677     def _encode_linux_xattr(self):
678         if self.linux_xattr:
679             result = vint.pack('V', len(self.linux_xattr))
680             for name, value in self.linux_xattr:
681                 result += vint.pack('ss', name, value)
682             return result
683         else:
684             return None
685
686     def _load_linux_xattr_rec(self, file):
687         data = vint.read_bvec(file)
688         memfile = BytesIO(data)
689         result = []
690         for i in range(vint.read_vuint(memfile)):
691             key = vint.read_bvec(memfile)
692             value = vint.read_bvec(memfile)
693             result.append((key, value))
694         self.linux_xattr = result
695
696     def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
697         if not xattr:
698             if self.linux_xattr:
699                 add_error("%s: can't restore xattr; xattr support missing.\n"
700                           % path)
701             return
702         if not self.linux_xattr:
703             return
704         try:
705             existing_xattrs = set(xattr.list(path, nofollow=True))
706         except IOError as e:
707             if e.errno == errno.EACCES:
708                 raise ApplyError('xattr.set %r: %s' % (path, e))
709             else:
710                 raise
711         for k, v in self.linux_xattr:
712             if k not in existing_xattrs \
713                     or v != xattr.get(path, k, nofollow=True):
714                 try:
715                     xattr.set(path, k, v, nofollow=True)
716                 except IOError as e:
717                     if e.errno == errno.EPERM \
718                             or e.errno == errno.EOPNOTSUPP:
719                         raise ApplyError('xattr.set %r: %s' % (path, e))
720                     else:
721                         raise
722             existing_xattrs -= frozenset([k])
723         for k in existing_xattrs:
724             try:
725                 xattr.remove(path, k, nofollow=True)
726             except IOError as e:
727                 if e.errno in (errno.EPERM, errno.EACCES):
728                     raise ApplyError('xattr.remove %r: %s' % (path, e))
729                 else:
730                     raise
731
732     def __init__(self):
733         self.mode = self.uid = self.gid = self.user = self.group = None
734         self.atime = self.mtime = self.ctime = None
735         # optional members
736         self.path = None
737         self.size = None
738         self.symlink_target = None
739         self.hardlink_target = None
740         self.linux_attr = None
741         self.linux_xattr = None
742         self.posix1e_acl = None
743
744     def __eq__(self, other):
745         if not isinstance(other, Metadata): return False
746         if self.mode != other.mode: return False
747         if self.mtime != other.mtime: return False
748         if self.ctime != other.ctime: return False
749         if self.atime != other.atime: return False
750         if self.path != other.path: return False
751         if self.uid != other.uid: return False
752         if self.gid != other.gid: return False
753         if self.size != other.size: return False
754         if self.user != other.user: return False
755         if self.group != other.group: return False
756         if self.symlink_target != other.symlink_target: return False
757         if self.hardlink_target != other.hardlink_target: return False
758         if self.linux_attr != other.linux_attr: return False
759         if self.posix1e_acl != other.posix1e_acl: return False
760         return True
761
762     def __ne__(self, other):
763         return not self.__eq__(other)
764
765     def __hash__(self):
766         return hash((self.mode,
767                      self.mtime,
768                      self.ctime,
769                      self.atime,
770                      self.path,
771                      self.uid,
772                      self.gid,
773                      self.size,
774                      self.user,
775                      self.group,
776                      self.symlink_target,
777                      self.hardlink_target,
778                      self.linux_attr,
779                      self.posix1e_acl))
780
781     def __repr__(self):
782         result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
783         if self.path is not None:
784             result += ' path:' + repr(self.path)
785         if self.mode is not None:
786             result += ' mode:' + repr(xstat.mode_str(self.mode)
787                                       + '(%s)' % oct(self.mode))
788         if self.uid is not None:
789             result += ' uid:' + str(self.uid)
790         if self.gid is not None:
791             result += ' gid:' + str(self.gid)
792         if self.user is not None:
793             result += ' user:' + repr(self.user)
794         if self.group is not None:
795             result += ' group:' + repr(self.group)
796         if self.size is not None:
797             result += ' size:' + repr(self.size)
798         for name, val in (('atime', self.atime),
799                           ('mtime', self.mtime),
800                           ('ctime', self.ctime)):
801             if val is not None:
802                 result += ' %s:%r (%d)' \
803                           % (name,
804                              strftime('%Y-%m-%d %H:%M %z',
805                                       gmtime(xstat.fstime_floor_secs(val))),
806                              val)
807         result += '>'
808         return ''.join(result)
809
810     def write(self, port, include_path=True):
811         records = include_path and [(_rec_tag_path, self._encode_path())] or []
812         records.extend([(_rec_tag_common_v3, self._encode_common()),
813                         (_rec_tag_symlink_target,
814                          self._encode_symlink_target()),
815                         (_rec_tag_hardlink_target,
816                          self._encode_hardlink_target()),
817                         (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
818                         (_rec_tag_linux_attr, self._encode_linux_attr()),
819                         (_rec_tag_linux_xattr, self._encode_linux_xattr())])
820         for tag, data in records:
821             if data:
822                 vint.write_vuint(port, tag)
823                 vint.write_bvec(port, data)
824         vint.write_vuint(port, _rec_tag_end)
825
826     def encode(self, include_path=True):
827         port = BytesIO()
828         self.write(port, include_path)
829         return port.getvalue()
830
831     def copy(self):
832         return deepcopy(self)
833
834     @staticmethod
835     def read(port):
836         # This method should either return a valid Metadata object,
837         # return None if there was no information at all (just a
838         # _rec_tag_end), throw EOFError if there was nothing at all to
839         # read, or throw an Exception if a valid object could not be
840         # read completely.
841         tag = vint.read_vuint(port)
842         if tag == _rec_tag_end:
843             return None
844         try: # From here on, EOF is an error.
845             result = Metadata()
846             while True: # only exit is error (exception) or _rec_tag_end
847                 if tag == _rec_tag_path:
848                     result._load_path_rec(port)
849                 elif tag == _rec_tag_common_v3:
850                     result._load_common_rec(port, version=3)
851                 elif tag == _rec_tag_common_v2:
852                     result._load_common_rec(port, version=2)
853                 elif tag == _rec_tag_symlink_target:
854                     result._load_symlink_target_rec(port)
855                 elif tag == _rec_tag_hardlink_target:
856                     result._load_hardlink_target_rec(port)
857                 elif tag == _rec_tag_posix1e_acl:
858                     result._load_posix1e_acl_rec(port)
859                 elif tag == _rec_tag_linux_attr:
860                     result._load_linux_attr_rec(port)
861                 elif tag == _rec_tag_linux_xattr:
862                     result._load_linux_xattr_rec(port)
863                 elif tag == _rec_tag_end:
864                     return result
865                 elif tag == _rec_tag_common_v1: # Should be very rare.
866                     result._load_common_rec(port, version=1)
867                 else: # unknown record
868                     vint.skip_bvec(port)
869                 tag = vint.read_vuint(port)
870         except EOFError:
871             raise Exception("EOF while reading Metadata")
872
873     def isdir(self):
874         return stat.S_ISDIR(self.mode)
875
876     def create_path(self, path, create_symlinks=True):
877         self._create_via_common_rec(path, create_symlinks=create_symlinks)
878
879     def apply_to_path(self, path=None, restore_numeric_ids=False):
880         # apply metadata to path -- file must exist
881         if not path:
882             path = self.path
883         if not path:
884             raise Exception('Metadata.apply_to_path() called with no path')
885         if not self._recognized_file_type():
886             add_error('not applying metadata to "%s"' % path
887                       + ' with unrecognized mode "0x%x"\n' % self.mode)
888             return
889         num_ids = restore_numeric_ids
890         for apply_metadata in (self._apply_common_rec,
891                                self._apply_posix1e_acl_rec,
892                                self._apply_linux_attr_rec,
893                                self._apply_linux_xattr_rec):
894             try:
895                 apply_metadata(path, restore_numeric_ids=num_ids)
896             except ApplyError as e:
897                 add_error(e)
898
899     def same_file(self, other):
900         """Compare this to other for equivalency.  Return true if
901         their information implies they could represent the same file
902         on disk, in the hardlink sense.  Assume they're both regular
903         files."""
904         return self._same_common(other) \
905             and self._same_hardlink_target(other) \
906             and self._same_posix1e_acl(other) \
907             and self._same_linux_attr(other) \
908             and self._same_linux_xattr(other)
909
910
911 def from_path(path, statinfo=None, archive_path=None,
912               save_symlinks=True, hardlink_target=None,
913               normalized=False):
914     """Return the metadata associated with the path.  When normalized is
915     true, return the metadata appropriate for a typical save, which
916     may or may not be all of it."""
917     result = Metadata()
918     result.path = archive_path
919     st = statinfo or xstat.lstat(path)
920     result._add_common(path, st)
921     if save_symlinks:
922         result._add_symlink_target(path, st)
923     result._add_hardlink_target(hardlink_target)
924     result._add_posix1e_acl(path, st)
925     result._add_linux_attr(path, st)
926     result._add_linux_xattr(path, st)
927     if normalized:
928         # Only store sizes for regular files and symlinks for now.
929         if not (stat.S_ISREG(result.mode) or stat.S_ISLNK(result.mode)):
930             result.size = None
931     return result
932
933
934 def save_tree(output_file, paths,
935               recurse=False,
936               write_paths=True,
937               save_symlinks=True,
938               xdev=False):
939
940     # Issue top-level rewrite warnings.
941     for path in paths:
942         safe_path = _clean_up_path_for_archive(path)
943         if safe_path != path:
944             log('archiving "%s" as "%s"\n' % (path, safe_path))
945
946     if not recurse:
947         for p in paths:
948             safe_path = _clean_up_path_for_archive(p)
949             st = xstat.lstat(p)
950             if stat.S_ISDIR(st.st_mode):
951                 safe_path += '/'
952             m = from_path(p, statinfo=st, archive_path=safe_path,
953                           save_symlinks=save_symlinks)
954             if verbose:
955                 print(m.path, file=sys.stderr)
956             m.write(output_file, include_path=write_paths)
957     else:
958         start_dir = os.getcwd()
959         try:
960             for (p, st) in recursive_dirlist(paths, xdev=xdev):
961                 dirlist_dir = os.getcwd()
962                 os.chdir(start_dir)
963                 safe_path = _clean_up_path_for_archive(p)
964                 m = from_path(p, statinfo=st, archive_path=safe_path,
965                               save_symlinks=save_symlinks)
966                 if verbose:
967                     print(m.path, file=sys.stderr)
968                 m.write(output_file, include_path=write_paths)
969                 os.chdir(dirlist_dir)
970         finally:
971             os.chdir(start_dir)
972
973
974 def _set_up_path(meta, create_symlinks=True):
975     # Allow directories to exist as a special case -- might have
976     # been created by an earlier longer path.
977     if meta.isdir():
978         mkdirp(meta.path)
979     else:
980         parent = os.path.dirname(meta.path)
981         if parent:
982             mkdirp(parent)
983         meta.create_path(meta.path, create_symlinks=create_symlinks)
984
985
986 all_fields = frozenset(['path',
987                         'mode',
988                         'link-target',
989                         'rdev',
990                         'size',
991                         'uid',
992                         'gid',
993                         'user',
994                         'group',
995                         'atime',
996                         'mtime',
997                         'ctime',
998                         'linux-attr',
999                         'linux-xattr',
1000                         'posix1e-acl'])
1001
1002
1003 def summary_str(meta, numeric_ids = False, classification = None,
1004                 human_readable = False):
1005
1006     """Return a string containing the "ls -l" style listing for meta.
1007     Classification may be "all", "type", or None."""
1008     user_str = group_str = size_or_dev_str = '?'
1009     symlink_target = None
1010     if meta:
1011         name = meta.path
1012         mode_str = xstat.mode_str(meta.mode)
1013         symlink_target = meta.symlink_target
1014         mtime_secs = xstat.fstime_floor_secs(meta.mtime)
1015         mtime_str = strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
1016         if meta.user and not numeric_ids:
1017             user_str = meta.user
1018         elif meta.uid != None:
1019             user_str = str(meta.uid)
1020         if meta.group and not numeric_ids:
1021             group_str = meta.group
1022         elif meta.gid != None:
1023             group_str = str(meta.gid)
1024         if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
1025             if meta.rdev:
1026                 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
1027                                              os.minor(meta.rdev))
1028         elif meta.size != None:
1029             if human_readable:
1030                 size_or_dev_str = format_filesize(meta.size)
1031             else:
1032                 size_or_dev_str = str(meta.size)
1033         else:
1034             size_or_dev_str = '-'
1035         if classification:
1036             classification_str = \
1037                 xstat.classification_str(meta.mode, classification == 'all')
1038     else:
1039         mode_str = '?' * 10
1040         mtime_str = '????-??-?? ??:??'
1041         classification_str = '?'
1042
1043     name = name or ''
1044     if classification:
1045         name += classification_str
1046     if symlink_target:
1047         name += ' -> ' + meta.symlink_target
1048
1049     return '%-10s %-11s %11s %16s %s' % (mode_str,
1050                                          user_str + "/" + group_str,
1051                                          size_or_dev_str,
1052                                          mtime_str,
1053                                          name)
1054
1055
1056 def detailed_str(meta, fields = None):
1057     # FIXME: should optional fields be omitted, or empty i.e. "rdev:
1058     # 0", "link-target:", etc.
1059     if not fields:
1060         fields = all_fields
1061
1062     result = []
1063     if 'path' in fields:
1064         path = meta.path or ''
1065         result.append('path: ' + path)
1066     if 'mode' in fields:
1067         result.append('mode: %s (%s)' % (oct(meta.mode),
1068                                          xstat.mode_str(meta.mode)))
1069     if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1070         result.append('link-target: ' + meta.symlink_target)
1071     if 'rdev' in fields:
1072         if meta.rdev:
1073             result.append('rdev: %d,%d' % (os.major(meta.rdev),
1074                                            os.minor(meta.rdev)))
1075         else:
1076             result.append('rdev: 0')
1077     if 'size' in fields and meta.size is not None:
1078         result.append('size: ' + str(meta.size))
1079     if 'uid' in fields:
1080         result.append('uid: ' + str(meta.uid))
1081     if 'gid' in fields:
1082         result.append('gid: ' + str(meta.gid))
1083     if 'user' in fields:
1084         result.append('user: ' + meta.user)
1085     if 'group' in fields:
1086         result.append('group: ' + meta.group)
1087     if 'atime' in fields:
1088         # If we don't have xstat.lutime, that means we have to use
1089         # utime(), and utime() has no way to set the mtime/atime of a
1090         # symlink.  Thus, the mtime/atime of a symlink is meaningless,
1091         # so let's not report it.  (That way scripts comparing
1092         # before/after won't trigger.)
1093         if xstat.lutime or not stat.S_ISLNK(meta.mode):
1094             result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1095         else:
1096             result.append('atime: 0')
1097     if 'mtime' in fields:
1098         if xstat.lutime or not stat.S_ISLNK(meta.mode):
1099             result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1100         else:
1101             result.append('mtime: 0')
1102     if 'ctime' in fields:
1103         result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1104     if 'linux-attr' in fields and meta.linux_attr:
1105         result.append('linux-attr: ' + hex(meta.linux_attr))
1106     if 'linux-xattr' in fields and meta.linux_xattr:
1107         for name, value in meta.linux_xattr:
1108             result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1109     if 'posix1e-acl' in fields and meta.posix1e_acl:
1110         acl = meta.posix1e_acl[0]
1111         result.append('posix1e-acl: ' + acl + '\n')
1112         if stat.S_ISDIR(meta.mode):
1113             def_acl = meta.posix1e_acl[2]
1114             result.append('posix1e-acl-default: ' + def_acl + '\n')
1115     return '\n'.join(result)
1116
1117
1118 class _ArchiveIterator:
1119     def next(self):
1120         try:
1121             return Metadata.read(self._file)
1122         except EOFError:
1123             raise StopIteration()
1124
1125     def __iter__(self):
1126         return self
1127
1128     def __init__(self, file):
1129         self._file = file
1130
1131
1132 def display_archive(file):
1133     if verbose > 1:
1134         first_item = True
1135         for meta in _ArchiveIterator(file):
1136             if not first_item:
1137                 print()
1138             print(detailed_str(meta))
1139             first_item = False
1140     elif verbose > 0:
1141         for meta in _ArchiveIterator(file):
1142             print(summary_str(meta))
1143     elif verbose == 0:
1144         for meta in _ArchiveIterator(file):
1145             if not meta.path:
1146                 print('bup: no metadata path, but asked to only display path'
1147                      '(increase verbosity?)')
1148                 sys.exit(1)
1149             print(meta.path)
1150
1151
1152 def start_extract(file, create_symlinks=True):
1153     for meta in _ArchiveIterator(file):
1154         if not meta: # Hit end record.
1155             break
1156         if verbose:
1157             print(meta.path, file=sys.stderr)
1158         xpath = _clean_up_extract_path(meta.path)
1159         if not xpath:
1160             add_error(Exception('skipping risky path "%s"' % meta.path))
1161         else:
1162             meta.path = xpath
1163             _set_up_path(meta, create_symlinks=create_symlinks)
1164
1165
1166 def finish_extract(file, restore_numeric_ids=False):
1167     all_dirs = []
1168     for meta in _ArchiveIterator(file):
1169         if not meta: # Hit end record.
1170             break
1171         xpath = _clean_up_extract_path(meta.path)
1172         if not xpath:
1173             add_error(Exception('skipping risky path "%s"' % dir.path))
1174         else:
1175             if os.path.isdir(meta.path):
1176                 all_dirs.append(meta)
1177             else:
1178                 if verbose:
1179                     print(meta.path, file=sys.stderr)
1180                 meta.apply_to_path(path=xpath,
1181                                    restore_numeric_ids=restore_numeric_ids)
1182     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1183     for dir in all_dirs:
1184         # Don't need to check xpath -- won't be in all_dirs if not OK.
1185         xpath = _clean_up_extract_path(dir.path)
1186         if verbose:
1187             print(dir.path, file=sys.stderr)
1188         dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1189
1190
1191 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1192     # For now, just store all the directories and handle them last,
1193     # longest first.
1194     all_dirs = []
1195     for meta in _ArchiveIterator(file):
1196         if not meta: # Hit end record.
1197             break
1198         xpath = _clean_up_extract_path(meta.path)
1199         if not xpath:
1200             add_error(Exception('skipping risky path "%s"' % meta.path))
1201         else:
1202             meta.path = xpath
1203             if verbose:
1204                 print('+', meta.path, file=sys.stderr)
1205             _set_up_path(meta, create_symlinks=create_symlinks)
1206             if os.path.isdir(meta.path):
1207                 all_dirs.append(meta)
1208             else:
1209                 if verbose:
1210                     print('=', meta.path, file=sys.stderr)
1211                 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1212     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1213     for dir in all_dirs:
1214         # Don't need to check xpath -- won't be in all_dirs if not OK.
1215         xpath = _clean_up_extract_path(dir.path)
1216         if verbose:
1217             print('=', xpath, file=sys.stderr)
1218         # Shouldn't have to check for risky paths here (omitted above).
1219         dir.apply_to_path(path=dir.path,
1220                           restore_numeric_ids=restore_numeric_ids)