]> arthur.barton.de Git - bup.git/blob - lib/bup/metadata.py
Honor git config pack.packSizeLimit when set
[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         self.symlink_target = vint.read_bvec(port)
467
468
469     ## Hardlink targets
470
471     def _add_hardlink_target(self, target):
472         self.hardlink_target = target
473
474     def _same_hardlink_target(self, other):
475         """Return true or false to indicate similarity in the hardlink sense."""
476         return self.hardlink_target == other.hardlink_target
477
478     def _encode_hardlink_target(self):
479         return self.hardlink_target
480
481     def _load_hardlink_target_rec(self, port):
482         self.hardlink_target = vint.read_bvec(port)
483
484
485     ## POSIX1e ACL records
486
487     # Recorded as a list:
488     #   [txt_id_acl, num_id_acl]
489     # or, if a directory:
490     #   [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
491     # The numeric/text distinction only matters when reading/restoring
492     # a stored record.
493     def _add_posix1e_acl(self, path, st):
494         if not posix1e or not posix1e.HAS_EXTENDED_CHECK:
495             return
496         if not stat.S_ISLNK(st.st_mode):
497             acls = None
498             def_acls = None
499             try:
500                 if posix1e.has_extended(path):
501                     acl = posix1e.ACL(file=path)
502                     acls = [acl, acl] # txt and num are the same
503                     if stat.S_ISDIR(st.st_mode):
504                         def_acl = posix1e.ACL(filedef=path)
505                         def_acls = [def_acl, def_acl]
506             except EnvironmentError as e:
507                 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
508                     raise
509             if acls:
510                 txt_flags = posix1e.TEXT_ABBREVIATE
511                 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
512                 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
513                            acls[1].to_any_text('', '\n', num_flags)]
514                 if def_acls:
515                     acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
516                     acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
517                 self.posix1e_acl = acl_rep
518
519     def _same_posix1e_acl(self, other):
520         """Return true or false to indicate similarity in the hardlink sense."""
521         return self.posix1e_acl == other.posix1e_acl
522
523     def _encode_posix1e_acl(self):
524         # Encode as two strings (w/default ACL string possibly empty).
525         if self.posix1e_acl:
526             acls = self.posix1e_acl
527             if len(acls) == 2:
528                 acls.extend(['', ''])
529             return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
530         else:
531             return None
532
533     def _load_posix1e_acl_rec(self, port):
534         acl_rep = vint.unpack('ssss', vint.read_bvec(port))
535         if acl_rep[2] == '':
536             acl_rep = acl_rep[:2]
537         self.posix1e_acl = acl_rep
538
539     def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
540         def apply_acl(acl_rep, kind):
541             try:
542                 acl = posix1e.ACL(text = acl_rep)
543             except IOError as e:
544                 if e.errno == 0:
545                     # pylibacl appears to return an IOError with errno
546                     # set to 0 if a group referred to by the ACL rep
547                     # doesn't exist on the current system.
548                     raise ApplyError("POSIX1e ACL: can't create %r for %r"
549                                      % (acl_rep, path))
550                 else:
551                     raise
552             try:
553                 acl.applyto(path, kind)
554             except IOError as e:
555                 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
556                     raise ApplyError('POSIX1e ACL applyto: %s' % e)
557                 else:
558                     raise
559
560         if not posix1e:
561             if self.posix1e_acl:
562                 add_error("%s: can't restore ACLs; posix1e support missing.\n"
563                           % path)
564             return
565         if self.posix1e_acl:
566             acls = self.posix1e_acl
567             if len(acls) > 2:
568                 if restore_numeric_ids:
569                     apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
570                 else:
571                     apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
572             if restore_numeric_ids:
573                 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
574             else:
575                 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
576
577
578     ## Linux attributes (lsattr(1), chattr(1))
579
580     def _add_linux_attr(self, path, st):
581         check_linux_file_attr_api()
582         if not get_linux_file_attr: return
583         if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
584             try:
585                 attr = get_linux_file_attr(path)
586                 if attr != 0:
587                     self.linux_attr = attr
588             except OSError as e:
589                 if e.errno == errno.EACCES:
590                     add_error('read Linux attr: %s' % e)
591                 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
592                     # Assume filesystem doesn't support attrs.
593                     return
594                 elif e.errno == EINVAL:
595                     global _warned_about_attr_einval
596                     if not _warned_about_attr_einval:
597                         log("Ignoring attr EINVAL;"
598                             + " if you're not using ntfs-3g, please report: "
599                             + repr(path) + '\n')
600                         _warned_about_attr_einval = True
601                     return
602                 else:
603                     raise
604
605     def _same_linux_attr(self, other):
606         """Return true or false to indicate similarity in the hardlink sense."""
607         return self.linux_attr == other.linux_attr
608
609     def _encode_linux_attr(self):
610         if self.linux_attr:
611             return vint.pack('V', self.linux_attr)
612         else:
613             return None
614
615     def _load_linux_attr_rec(self, port):
616         data = vint.read_bvec(port)
617         self.linux_attr = vint.unpack('V', data)[0]
618
619     def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
620         if self.linux_attr:
621             check_linux_file_attr_api()
622             if not set_linux_file_attr:
623                 add_error("%s: can't restore linuxattrs: "
624                           "linuxattr support missing.\n" % path)
625                 return
626             try:
627                 set_linux_file_attr(path, self.linux_attr)
628             except OSError as e:
629                 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
630                     raise ApplyError('Linux chattr: %s (0x%s)'
631                                      % (e, hex(self.linux_attr)))
632                 elif e.errno == EINVAL:
633                     msg = "if you're not using ntfs-3g, please report"
634                     raise ApplyError('Linux chattr: %s (0x%s) (%s)'
635                                      % (e, hex(self.linux_attr), msg))
636                 else:
637                     raise
638
639
640     ## Linux extended attributes (getfattr(1), setfattr(1))
641
642     def _add_linux_xattr(self, path, st):
643         if not xattr: return
644         try:
645             self.linux_xattr = xattr.get_all(path, nofollow=True)
646         except EnvironmentError as e:
647             if e.errno != errno.EOPNOTSUPP:
648                 raise
649
650     def _same_linux_xattr(self, other):
651         """Return true or false to indicate similarity in the hardlink sense."""
652         return self.linux_xattr == other.linux_xattr
653
654     def _encode_linux_xattr(self):
655         if self.linux_xattr:
656             result = vint.pack('V', len(self.linux_xattr))
657             for name, value in self.linux_xattr:
658                 result += vint.pack('ss', name, value)
659             return result
660         else:
661             return None
662
663     def _load_linux_xattr_rec(self, file):
664         data = vint.read_bvec(file)
665         memfile = BytesIO(data)
666         result = []
667         for i in range(vint.read_vuint(memfile)):
668             key = vint.read_bvec(memfile)
669             value = vint.read_bvec(memfile)
670             result.append((key, value))
671         self.linux_xattr = result
672
673     def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
674         if not xattr:
675             if self.linux_xattr:
676                 add_error("%s: can't restore xattr; xattr support missing.\n"
677                           % path)
678             return
679         if not self.linux_xattr:
680             return
681         try:
682             existing_xattrs = set(xattr.list(path, nofollow=True))
683         except IOError as e:
684             if e.errno == errno.EACCES:
685                 raise ApplyError('xattr.set %r: %s' % (path, e))
686             else:
687                 raise
688         for k, v in self.linux_xattr:
689             if k not in existing_xattrs \
690                     or v != xattr.get(path, k, nofollow=True):
691                 try:
692                     xattr.set(path, k, v, nofollow=True)
693                 except IOError as e:
694                     if e.errno == errno.EPERM \
695                             or e.errno == errno.EOPNOTSUPP:
696                         raise ApplyError('xattr.set %r: %s' % (path, e))
697                     else:
698                         raise
699             existing_xattrs -= frozenset([k])
700         for k in existing_xattrs:
701             try:
702                 xattr.remove(path, k, nofollow=True)
703             except IOError as e:
704                 if e.errno in (errno.EPERM, errno.EACCES):
705                     raise ApplyError('xattr.remove %r: %s' % (path, e))
706                 else:
707                     raise
708
709     def __init__(self):
710         self.mode = self.uid = self.gid = self.user = self.group = None
711         self.atime = self.mtime = self.ctime = None
712         # optional members
713         self.path = None
714         self.size = None
715         self.symlink_target = None
716         self.hardlink_target = None
717         self.linux_attr = None
718         self.linux_xattr = None
719         self.posix1e_acl = None
720
721     def __repr__(self):
722         result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
723         if self.path:
724             result += ' path:' + repr(self.path)
725         if self.mode:
726             result += ' mode:' + repr(xstat.mode_str(self.mode)
727                                       + '(%s)' % hex(self.mode))
728         if self.uid:
729             result += ' uid:' + str(self.uid)
730         if self.gid:
731             result += ' gid:' + str(self.gid)
732         if self.user:
733             result += ' user:' + repr(self.user)
734         if self.group:
735             result += ' group:' + repr(self.group)
736         if self.size:
737             result += ' size:' + repr(self.size)
738         for name, val in (('atime', self.atime),
739                           ('mtime', self.mtime),
740                           ('ctime', self.ctime)):
741             result += ' %s:%r' \
742                 % (name,
743                    time.strftime('%Y-%m-%d %H:%M %z',
744                                  time.gmtime(xstat.fstime_floor_secs(val))))
745         result += '>'
746         return ''.join(result)
747
748     def write(self, port, include_path=True):
749         records = include_path and [(_rec_tag_path, self._encode_path())] or []
750         records.extend([(_rec_tag_common_v2, self._encode_common()),
751                         (_rec_tag_symlink_target,
752                          self._encode_symlink_target()),
753                         (_rec_tag_hardlink_target,
754                          self._encode_hardlink_target()),
755                         (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
756                         (_rec_tag_linux_attr, self._encode_linux_attr()),
757                         (_rec_tag_linux_xattr, self._encode_linux_xattr())])
758         for tag, data in records:
759             if data:
760                 vint.write_vuint(port, tag)
761                 vint.write_bvec(port, data)
762         vint.write_vuint(port, _rec_tag_end)
763
764     def encode(self, include_path=True):
765         port = BytesIO()
766         self.write(port, include_path)
767         return port.getvalue()
768
769     @staticmethod
770     def read(port):
771         # This method should either return a valid Metadata object,
772         # return None if there was no information at all (just a
773         # _rec_tag_end), throw EOFError if there was nothing at all to
774         # read, or throw an Exception if a valid object could not be
775         # read completely.
776         tag = vint.read_vuint(port)
777         if tag == _rec_tag_end:
778             return None
779         try: # From here on, EOF is an error.
780             result = Metadata()
781             while True: # only exit is error (exception) or _rec_tag_end
782                 if tag == _rec_tag_path:
783                     result._load_path_rec(port)
784                 elif tag == _rec_tag_common_v2:
785                     result._load_common_rec(port)
786                 elif tag == _rec_tag_symlink_target:
787                     result._load_symlink_target_rec(port)
788                 elif tag == _rec_tag_hardlink_target:
789                     result._load_hardlink_target_rec(port)
790                 elif tag == _rec_tag_posix1e_acl:
791                     result._load_posix1e_acl_rec(port)
792                 elif tag == _rec_tag_linux_attr:
793                     result._load_linux_attr_rec(port)
794                 elif tag == _rec_tag_linux_xattr:
795                     result._load_linux_xattr_rec(port)
796                 elif tag == _rec_tag_end:
797                     return result
798                 elif tag == _rec_tag_common: # Should be very rare.
799                     result._load_common_rec(port, legacy_format = True)
800                 else: # unknown record
801                     vint.skip_bvec(port)
802                 tag = vint.read_vuint(port)
803         except EOFError:
804             raise Exception("EOF while reading Metadata")
805
806     def isdir(self):
807         return stat.S_ISDIR(self.mode)
808
809     def create_path(self, path, create_symlinks=True):
810         self._create_via_common_rec(path, create_symlinks=create_symlinks)
811
812     def apply_to_path(self, path=None, restore_numeric_ids=False):
813         # apply metadata to path -- file must exist
814         if not path:
815             path = self.path
816         if not path:
817             raise Exception('Metadata.apply_to_path() called with no path')
818         if not self._recognized_file_type():
819             add_error('not applying metadata to "%s"' % path
820                       + ' with unrecognized mode "0x%x"\n' % self.mode)
821             return
822         num_ids = restore_numeric_ids
823         for apply_metadata in (self._apply_common_rec,
824                                self._apply_posix1e_acl_rec,
825                                self._apply_linux_attr_rec,
826                                self._apply_linux_xattr_rec):
827             try:
828                 apply_metadata(path, restore_numeric_ids=num_ids)
829             except ApplyError as e:
830                 add_error(e)
831
832     def same_file(self, other):
833         """Compare this to other for equivalency.  Return true if
834         their information implies they could represent the same file
835         on disk, in the hardlink sense.  Assume they're both regular
836         files."""
837         return self._same_common(other) \
838             and self._same_hardlink_target(other) \
839             and self._same_posix1e_acl(other) \
840             and self._same_linux_attr(other) \
841             and self._same_linux_xattr(other)
842
843
844 def from_path(path, statinfo=None, archive_path=None,
845               save_symlinks=True, hardlink_target=None):
846     result = Metadata()
847     result.path = archive_path
848     st = statinfo or xstat.lstat(path)
849     result.size = st.st_size
850     result._add_common(path, st)
851     if save_symlinks:
852         result._add_symlink_target(path, st)
853     result._add_hardlink_target(hardlink_target)
854     result._add_posix1e_acl(path, st)
855     result._add_linux_attr(path, st)
856     result._add_linux_xattr(path, st)
857     return result
858
859
860 def save_tree(output_file, paths,
861               recurse=False,
862               write_paths=True,
863               save_symlinks=True,
864               xdev=False):
865
866     # Issue top-level rewrite warnings.
867     for path in paths:
868         safe_path = _clean_up_path_for_archive(path)
869         if safe_path != path:
870             log('archiving "%s" as "%s"\n' % (path, safe_path))
871
872     if not recurse:
873         for p in paths:
874             safe_path = _clean_up_path_for_archive(p)
875             st = xstat.lstat(p)
876             if stat.S_ISDIR(st.st_mode):
877                 safe_path += '/'
878             m = from_path(p, statinfo=st, archive_path=safe_path,
879                           save_symlinks=save_symlinks)
880             if verbose:
881                 print >> sys.stderr, m.path
882             m.write(output_file, include_path=write_paths)
883     else:
884         start_dir = os.getcwd()
885         try:
886             for (p, st) in recursive_dirlist(paths, xdev=xdev):
887                 dirlist_dir = os.getcwd()
888                 os.chdir(start_dir)
889                 safe_path = _clean_up_path_for_archive(p)
890                 m = from_path(p, statinfo=st, archive_path=safe_path,
891                               save_symlinks=save_symlinks)
892                 if verbose:
893                     print >> sys.stderr, m.path
894                 m.write(output_file, include_path=write_paths)
895                 os.chdir(dirlist_dir)
896         finally:
897             os.chdir(start_dir)
898
899
900 def _set_up_path(meta, create_symlinks=True):
901     # Allow directories to exist as a special case -- might have
902     # been created by an earlier longer path.
903     if meta.isdir():
904         mkdirp(meta.path)
905     else:
906         parent = os.path.dirname(meta.path)
907         if parent:
908             mkdirp(parent)
909         meta.create_path(meta.path, create_symlinks=create_symlinks)
910
911
912 all_fields = frozenset(['path',
913                         'mode',
914                         'link-target',
915                         'rdev',
916                         'size',
917                         'uid',
918                         'gid',
919                         'user',
920                         'group',
921                         'atime',
922                         'mtime',
923                         'ctime',
924                         'linux-attr',
925                         'linux-xattr',
926                         'posix1e-acl'])
927
928
929 def summary_str(meta, numeric_ids = False, classification = None,
930                 human_readable = False):
931
932     """Return a string containing the "ls -l" style listing for meta.
933     Classification may be "all", "type", or None."""
934     user_str = group_str = size_or_dev_str = '?'
935     symlink_target = None
936     if meta:
937         name = meta.path
938         mode_str = xstat.mode_str(meta.mode)
939         symlink_target = meta.symlink_target
940         mtime_secs = xstat.fstime_floor_secs(meta.mtime)
941         mtime_str = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
942         if meta.user and not numeric_ids:
943             user_str = meta.user
944         elif meta.uid != None:
945             user_str = str(meta.uid)
946         if meta.group and not numeric_ids:
947             group_str = meta.group
948         elif meta.gid != None:
949             group_str = str(meta.gid)
950         if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
951             if meta.rdev:
952                 size_or_dev_str = '%d,%d' % (os.major(meta.rdev),
953                                              os.minor(meta.rdev))
954         elif meta.size != None:
955             if human_readable:
956                 size_or_dev_str = format_filesize(meta.size)
957             else:
958                 size_or_dev_str = str(meta.size)
959         else:
960             size_or_dev_str = '-'
961         if classification:
962             classification_str = \
963                 xstat.classification_str(meta.mode, classification == 'all')
964     else:
965         mode_str = '?' * 10
966         mtime_str = '????-??-?? ??:??'
967         classification_str = '?'
968
969     name = name or ''
970     if classification:
971         name += classification_str
972     if symlink_target:
973         name += ' -> ' + meta.symlink_target
974
975     return '%-10s %-11s %11s %16s %s' % (mode_str,
976                                          user_str + "/" + group_str,
977                                          size_or_dev_str,
978                                          mtime_str,
979                                          name)
980
981
982 def detailed_str(meta, fields = None):
983     # FIXME: should optional fields be omitted, or empty i.e. "rdev:
984     # 0", "link-target:", etc.
985     if not fields:
986         fields = all_fields
987
988     result = []
989     if 'path' in fields:
990         path = meta.path or ''
991         result.append('path: ' + path)
992     if 'mode' in fields:
993         result.append('mode: %s (%s)' % (oct(meta.mode),
994                                          xstat.mode_str(meta.mode)))
995     if 'link-target' in fields and stat.S_ISLNK(meta.mode):
996         result.append('link-target: ' + meta.symlink_target)
997     if 'rdev' in fields:
998         if meta.rdev:
999             result.append('rdev: %d,%d' % (os.major(meta.rdev),
1000                                            os.minor(meta.rdev)))
1001         else:
1002             result.append('rdev: 0')
1003     if 'size' in fields and meta.size:
1004         result.append('size: ' + str(meta.size))
1005     if 'uid' in fields:
1006         result.append('uid: ' + str(meta.uid))
1007     if 'gid' in fields:
1008         result.append('gid: ' + str(meta.gid))
1009     if 'user' in fields:
1010         result.append('user: ' + meta.user)
1011     if 'group' in fields:
1012         result.append('group: ' + meta.group)
1013     if 'atime' in fields:
1014         # If we don't have xstat.lutime, that means we have to use
1015         # utime(), and utime() has no way to set the mtime/atime of a
1016         # symlink.  Thus, the mtime/atime of a symlink is meaningless,
1017         # so let's not report it.  (That way scripts comparing
1018         # before/after won't trigger.)
1019         if xstat.lutime or not stat.S_ISLNK(meta.mode):
1020             result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
1021         else:
1022             result.append('atime: 0')
1023     if 'mtime' in fields:
1024         if xstat.lutime or not stat.S_ISLNK(meta.mode):
1025             result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
1026         else:
1027             result.append('mtime: 0')
1028     if 'ctime' in fields:
1029         result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
1030     if 'linux-attr' in fields and meta.linux_attr:
1031         result.append('linux-attr: ' + hex(meta.linux_attr))
1032     if 'linux-xattr' in fields and meta.linux_xattr:
1033         for name, value in meta.linux_xattr:
1034             result.append('linux-xattr: %s -> %s' % (name, repr(value)))
1035     if 'posix1e-acl' in fields and meta.posix1e_acl:
1036         acl = meta.posix1e_acl[0]
1037         result.append('posix1e-acl: ' + acl + '\n')
1038         if stat.S_ISDIR(meta.mode):
1039             def_acl = meta.posix1e_acl[2]
1040             result.append('posix1e-acl-default: ' + def_acl + '\n')
1041     return '\n'.join(result)
1042
1043
1044 class _ArchiveIterator:
1045     def next(self):
1046         try:
1047             return Metadata.read(self._file)
1048         except EOFError:
1049             raise StopIteration()
1050
1051     def __iter__(self):
1052         return self
1053
1054     def __init__(self, file):
1055         self._file = file
1056
1057
1058 def display_archive(file):
1059     if verbose > 1:
1060         first_item = True
1061         for meta in _ArchiveIterator(file):
1062             if not first_item:
1063                 print
1064             print detailed_str(meta)
1065             first_item = False
1066     elif verbose > 0:
1067         for meta in _ArchiveIterator(file):
1068             print summary_str(meta)
1069     elif verbose == 0:
1070         for meta in _ArchiveIterator(file):
1071             if not meta.path:
1072                 print >> sys.stderr, \
1073                     'bup: no metadata path, but asked to only display path', \
1074                     '(increase verbosity?)'
1075                 sys.exit(1)
1076             print meta.path
1077
1078
1079 def start_extract(file, create_symlinks=True):
1080     for meta in _ArchiveIterator(file):
1081         if not meta: # Hit end record.
1082             break
1083         if verbose:
1084             print >> sys.stderr, meta.path
1085         xpath = _clean_up_extract_path(meta.path)
1086         if not xpath:
1087             add_error(Exception('skipping risky path "%s"' % meta.path))
1088         else:
1089             meta.path = xpath
1090             _set_up_path(meta, create_symlinks=create_symlinks)
1091
1092
1093 def finish_extract(file, restore_numeric_ids=False):
1094     all_dirs = []
1095     for meta in _ArchiveIterator(file):
1096         if not meta: # Hit end record.
1097             break
1098         xpath = _clean_up_extract_path(meta.path)
1099         if not xpath:
1100             add_error(Exception('skipping risky path "%s"' % dir.path))
1101         else:
1102             if os.path.isdir(meta.path):
1103                 all_dirs.append(meta)
1104             else:
1105                 if verbose:
1106                     print >> sys.stderr, meta.path
1107                 meta.apply_to_path(path=xpath,
1108                                    restore_numeric_ids=restore_numeric_ids)
1109     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1110     for dir in all_dirs:
1111         # Don't need to check xpath -- won't be in all_dirs if not OK.
1112         xpath = _clean_up_extract_path(dir.path)
1113         if verbose:
1114             print >> sys.stderr, dir.path
1115         dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1116
1117
1118 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1119     # For now, just store all the directories and handle them last,
1120     # longest first.
1121     all_dirs = []
1122     for meta in _ArchiveIterator(file):
1123         if not meta: # Hit end record.
1124             break
1125         xpath = _clean_up_extract_path(meta.path)
1126         if not xpath:
1127             add_error(Exception('skipping risky path "%s"' % meta.path))
1128         else:
1129             meta.path = xpath
1130             if verbose:
1131                 print >> sys.stderr, '+', meta.path
1132             _set_up_path(meta, create_symlinks=create_symlinks)
1133             if os.path.isdir(meta.path):
1134                 all_dirs.append(meta)
1135             else:
1136                 if verbose:
1137                     print >> sys.stderr, '=', meta.path
1138                 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1139     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1140     for dir in all_dirs:
1141         # Don't need to check xpath -- won't be in all_dirs if not OK.
1142         xpath = _clean_up_extract_path(dir.path)
1143         if verbose:
1144             print >> sys.stderr, '=', xpath
1145         # Shouldn't have to check for risky paths here (omitted above).
1146         dir.apply_to_path(path=dir.path,
1147                           restore_numeric_ids=restore_numeric_ids)