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