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