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