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