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