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