]> arthur.barton.de Git - bup.git/blob - lib/bup/metadata.py
Don't include atime when determining hardlink compatibility.
[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? (unimplemented)
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.atime = st.st_atime
203         self.mtime = st.st_mtime
204         self.ctime = st.st_ctime
205         self.user = self.group = ''
206         entry = pwd_from_uid(st.st_uid)
207         if entry:
208             self.user = entry.pw_name
209         entry = grp_from_gid(st.st_gid)
210         if entry:
211             self.group = entry.gr_name
212         self.mode = st.st_mode
213         # Only collect st_rdev if we might need it for a mknod()
214         # during restore.  On some platforms (i.e. kFreeBSD), it isn't
215         # stable for other file types.  For example "cp -a" will
216         # change it for a plain file.
217         if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
218             self.rdev = st.st_rdev
219         else:
220             self.rdev = 0
221
222     def _same_common(self, other):
223         """Return true or false to indicate similarity in the hardlink sense."""
224         return self.uid == other.uid \
225             and self.gid == other.gid \
226             and self.rdev == other.rdev \
227             and self.mtime == other.mtime \
228             and self.ctime == other.ctime \
229             and self.user == other.user \
230             and self.group == other.group
231
232     def _encode_common(self):
233         if not self.mode:
234             return None
235         atime = xstat.nsecs_to_timespec(self.atime)
236         mtime = xstat.nsecs_to_timespec(self.mtime)
237         ctime = xstat.nsecs_to_timespec(self.ctime)
238         result = vint.pack('VVsVsVvVvVvV',
239                            self.mode,
240                            self.uid,
241                            self.user,
242                            self.gid,
243                            self.group,
244                            self.rdev,
245                            atime[0],
246                            atime[1],
247                            mtime[0],
248                            mtime[1],
249                            ctime[0],
250                            ctime[1])
251         return result
252
253     def _load_common_rec(self, port):
254         data = vint.read_bvec(port)
255         (self.mode,
256          self.uid,
257          self.user,
258          self.gid,
259          self.group,
260          self.rdev,
261          self.atime,
262          atime_ns,
263          self.mtime,
264          mtime_ns,
265          self.ctime,
266          ctime_ns) = vint.unpack('VVsVsVvVvVvV', data)
267         self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
268         self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
269         self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
270
271     def _recognized_file_type(self):
272         return stat.S_ISREG(self.mode) \
273             or stat.S_ISDIR(self.mode) \
274             or stat.S_ISCHR(self.mode) \
275             or stat.S_ISBLK(self.mode) \
276             or stat.S_ISFIFO(self.mode) \
277             or stat.S_ISSOCK(self.mode) \
278             or stat.S_ISLNK(self.mode)
279
280     def _create_via_common_rec(self, path, create_symlinks=True):
281         if not self.mode:
282             raise ApplyError('no metadata - cannot create path ' + path)
283
284         # If the path already exists and is a dir, try rmdir.
285         # If the path already exists and is anything else, try unlink.
286         st = None
287         try:
288             st = xstat.lstat(path)
289         except OSError, e:
290             if e.errno != errno.ENOENT:
291                 raise
292         if st:
293             if stat.S_ISDIR(st.st_mode):
294                 try:
295                     os.rmdir(path)
296                 except OSError, e:
297                     if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
298                         msg = 'refusing to overwrite non-empty dir ' + path
299                         raise Exception(msg)
300                     raise
301             else:
302                 os.unlink(path)
303
304         if stat.S_ISREG(self.mode):
305             assert(self._recognized_file_type())
306             fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0600)
307             os.close(fd)
308         elif stat.S_ISDIR(self.mode):
309             assert(self._recognized_file_type())
310             os.mkdir(path, 0700)
311         elif stat.S_ISCHR(self.mode):
312             assert(self._recognized_file_type())
313             os.mknod(path, 0600 | stat.S_IFCHR, self.rdev)
314         elif stat.S_ISBLK(self.mode):
315             assert(self._recognized_file_type())
316             os.mknod(path, 0600 | stat.S_IFBLK, self.rdev)
317         elif stat.S_ISFIFO(self.mode):
318             assert(self._recognized_file_type())
319             os.mknod(path, 0600 | stat.S_IFIFO)
320         elif stat.S_ISSOCK(self.mode):
321             try:
322                 os.mknod(path, 0600 | stat.S_IFSOCK)
323             except OSError, e:
324                 if e.errno in (errno.EINVAL, errno.EPERM):
325                     s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
326                     s.bind(path)
327                 else:
328                     raise
329         elif stat.S_ISLNK(self.mode):
330             assert(self._recognized_file_type())
331             if self.symlink_target and create_symlinks:
332                 # on MacOS, symlink() permissions depend on umask, and there's
333                 # no way to chown a symlink after creating it, so we have to
334                 # be careful here!
335                 oldumask = os.umask((self.mode & 0777) ^ 0777)
336                 try:
337                     os.symlink(self.symlink_target, path)
338                 finally:
339                     os.umask(oldumask)
340         # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
341         else:
342             assert(not self._recognized_file_type())
343             add_error('not creating "%s" with unrecognized mode "0x%x"\n'
344                       % (path, self.mode))
345
346     def _apply_common_rec(self, path, restore_numeric_ids=False):
347         if not self.mode:
348             raise ApplyError('no metadata - cannot apply to ' + path)
349
350         # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
351         # EACCES errors at this stage are fatal for the current path.
352         if lutime and stat.S_ISLNK(self.mode):
353             try:
354                 lutime(path, (self.atime, self.mtime))
355             except OSError, e:
356                 if e.errno == errno.EACCES:
357                     raise ApplyError('lutime: %s' % e)
358                 else:
359                     raise
360         else:
361             try:
362                 utime(path, (self.atime, self.mtime))
363             except OSError, e:
364                 if e.errno == errno.EACCES:
365                     raise ApplyError('utime: %s' % e)
366                 else:
367                     raise
368
369         # Implement tar/rsync-like semantics; see bup-restore(1).
370         # FIXME: should we consider caching user/group name <-> id
371         # mappings, getgroups(), etc.?
372         uid = gid = -1 # By default, do nothing.
373         if is_superuser():
374             uid = self.uid
375             gid = self.gid
376             if not restore_numeric_ids:
377                 if self.uid != 0 and self.user:
378                     entry = pwd_from_name(self.user)
379                     if entry:
380                         uid = entry.pw_uid
381                 if self.gid != 0 and self.group:
382                     entry = grp_from_name(self.group)
383                     if entry:
384                         gid = entry.gr_gid
385         else: # not superuser - only consider changing the group/gid
386             user_gids = os.getgroups()
387             if self.gid in user_gids:
388                 gid = self.gid
389             if not restore_numeric_ids and self.gid != 0:
390                 # The grp might not exist on the local system.
391                 grps = filter(None, [grp_from_gid(x) for x in user_gids])
392                 if self.group in [x.gr_name for x in grps]:
393                     g = grp_from_name(self.group)
394                     if g:
395                         gid = g.gr_gid
396
397         if uid != -1 or gid != -1:
398             try:
399                 os.lchown(path, uid, gid)
400             except OSError, e:
401                 if e.errno == errno.EPERM:
402                     add_error('lchown: %s' %  e)
403                 elif sys.platform.startswith('cygwin') \
404                    and e.errno == errno.EINVAL:
405                     add_error('lchown: unknown uid/gid (%d/%d) for %s'
406                               %  (uid, gid, path))
407                 else:
408                     raise
409
410         if _have_lchmod:
411             os.lchmod(path, stat.S_IMODE(self.mode))
412         elif not stat.S_ISLNK(self.mode):
413             os.chmod(path, stat.S_IMODE(self.mode))
414
415
416     ## Path records
417
418     def _encode_path(self):
419         if self.path:
420             return vint.pack('s', self.path)
421         else:
422             return None
423
424     def _load_path_rec(self, port):
425         self.path = vint.unpack('s', vint.read_bvec(port))[0]
426
427
428     ## Symlink targets
429
430     def _add_symlink_target(self, path, st):
431         try:
432             if stat.S_ISLNK(st.st_mode):
433                 self.symlink_target = os.readlink(path)
434         except OSError, e:
435             add_error('readlink: %s', e)
436
437     def _encode_symlink_target(self):
438         return self.symlink_target
439
440     def _load_symlink_target_rec(self, port):
441         self.symlink_target = vint.read_bvec(port)
442
443
444     ## Hardlink targets
445
446     def _add_hardlink_target(self, target):
447         self.hardlink_target = target
448
449     def _same_hardlink_target(self, other):
450         """Return true or false to indicate similarity in the hardlink sense."""
451         return self.hardlink_target == other.hardlink_target
452
453     def _encode_hardlink_target(self):
454         return self.hardlink_target
455
456     def _load_hardlink_target_rec(self, port):
457         self.hardlink_target = vint.read_bvec(port)
458
459
460     ## POSIX1e ACL records
461
462     # Recorded as a list:
463     #   [txt_id_acl, num_id_acl]
464     # or, if a directory:
465     #   [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
466     # The numeric/text distinction only matters when reading/restoring
467     # a stored record.
468     def _add_posix1e_acl(self, path, st):
469         if not posix1e: return
470         if not stat.S_ISLNK(st.st_mode):
471             acls = None
472             def_acls = None
473             try:
474                 if posix1e.has_extended(path):
475                     acl = posix1e.ACL(file=path)
476                     acls = [acl, acl] # txt and num are the same
477                     if stat.S_ISDIR(st.st_mode):
478                         def_acl = posix1e.ACL(filedef=path)
479                         def_acls = [def_acl, def_acl]
480             except EnvironmentError, e:
481                 if e.errno not in (errno.EOPNOTSUPP, errno.ENOSYS):
482                     raise
483             if acls:
484                 txt_flags = posix1e.TEXT_ABBREVIATE
485                 num_flags = posix1e.TEXT_ABBREVIATE | posix1e.TEXT_NUMERIC_IDS
486                 acl_rep = [acls[0].to_any_text('', '\n', txt_flags),
487                            acls[1].to_any_text('', '\n', num_flags)]
488                 if def_acls:
489                     acl_rep.append(def_acls[0].to_any_text('', '\n', txt_flags))
490                     acl_rep.append(def_acls[1].to_any_text('', '\n', num_flags))
491                 self.posix1e_acl = acl_rep
492
493     def _same_posix1e_acl(self, other):
494         """Return true or false to indicate similarity in the hardlink sense."""
495         return self.posix1e_acl == other.posix1e_acl
496
497     def _encode_posix1e_acl(self):
498         # Encode as two strings (w/default ACL string possibly empty).
499         if self.posix1e_acl:
500             acls = self.posix1e_acl
501             if len(acls) == 2:
502                 acls.extend(['', ''])
503             return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
504         else:
505             return None
506
507     def _load_posix1e_acl_rec(self, port):
508         acl_rep = vint.unpack('ssss', vint.read_bvec(port))
509         if acl_rep[2] == '':
510             acl_rep = acl_rep[:2]
511         self.posix1e_acl = acl_rep
512
513     def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
514         def apply_acl(acl_rep, kind):
515             try:
516                 acl = posix1e.ACL(text = acl_rep)
517             except IOError, e:
518                 if e.errno == 0:
519                     # pylibacl appears to return an IOError with errno
520                     # set to 0 if a group referred to by the ACL rep
521                     # doesn't exist on the current system.
522                     raise ApplyError("POSIX1e ACL: can't create %r for %r"
523                                      % (acl_rep, path))
524                 else:
525                     raise
526             try:
527                 acl.applyto(path, kind)
528             except IOError, e:
529                 if e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
530                     raise ApplyError('POSIX1e ACL applyto: %s' % e)
531                 else:
532                     raise
533
534         if not posix1e:
535             if self.posix1e_acl:
536                 add_error("%s: can't restore ACLs; posix1e support missing.\n"
537                           % path)
538             return
539         if self.posix1e_acl:
540             acls = self.posix1e_acl
541             if len(acls) > 2:
542                 if restore_numeric_ids:
543                     apply_acl(acls[3], posix1e.ACL_TYPE_DEFAULT)
544                 else:
545                     apply_acl(acls[2], posix1e.ACL_TYPE_DEFAULT)
546             if restore_numeric_ids:
547                 apply_acl(acls[1], posix1e.ACL_TYPE_ACCESS)
548             else:
549                 apply_acl(acls[0], posix1e.ACL_TYPE_ACCESS)
550
551
552     ## Linux attributes (lsattr(1), chattr(1))
553
554     def _add_linux_attr(self, path, st):
555         if not get_linux_file_attr: return
556         if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
557             try:
558                 attr = get_linux_file_attr(path)
559                 if attr != 0:
560                     self.linux_attr = attr
561             except OSError, e:
562                 if e.errno == errno.EACCES:
563                     add_error('read Linux attr: %s' % e)
564                 elif e.errno in (errno.ENOTTY, errno.ENOSYS, errno.EOPNOTSUPP):
565                     # Assume filesystem doesn't support attrs.
566                     return
567                 else:
568                     raise
569
570     def _same_linux_attr(self, other):
571         """Return true or false to indicate similarity in the hardlink sense."""
572         return self.linux_attr == other.linux_attr
573
574     def _encode_linux_attr(self):
575         if self.linux_attr:
576             return vint.pack('V', self.linux_attr)
577         else:
578             return None
579
580     def _load_linux_attr_rec(self, port):
581         data = vint.read_bvec(port)
582         self.linux_attr = vint.unpack('V', data)[0]
583
584     def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
585         if self.linux_attr:
586             if not set_linux_file_attr:
587                 add_error("%s: can't restore linuxattrs: "
588                           "linuxattr support missing.\n" % path)
589                 return
590             try:
591                 set_linux_file_attr(path, self.linux_attr)
592             except OSError, e:
593                 if e.errno in (errno.ENOTTY, errno.EOPNOTSUPP, errno.ENOSYS,
594                                errno.EACCES):
595                     raise ApplyError('Linux chattr: %s (0x%s)'
596                                      % (e, hex(self.linux_attr)))
597                 else:
598                     raise
599
600
601     ## Linux extended attributes (getfattr(1), setfattr(1))
602
603     def _add_linux_xattr(self, path, st):
604         if not xattr: return
605         try:
606             self.linux_xattr = xattr.get_all(path, nofollow=True)
607         except EnvironmentError, e:
608             if e.errno != errno.EOPNOTSUPP:
609                 raise
610
611     def _same_linux_xattr(self, other):
612         """Return true or false to indicate similarity in the hardlink sense."""
613         return self.linux_xattr == other.linux_xattr
614
615     def _encode_linux_xattr(self):
616         if self.linux_xattr:
617             result = vint.pack('V', len(self.linux_xattr))
618             for name, value in self.linux_xattr:
619                 result += vint.pack('ss', name, value)
620             return result
621         else:
622             return None
623
624     def _load_linux_xattr_rec(self, file):
625         data = vint.read_bvec(file)
626         memfile = StringIO(data)
627         result = []
628         for i in range(vint.read_vuint(memfile)):
629             key = vint.read_bvec(memfile)
630             value = vint.read_bvec(memfile)
631             result.append((key, value))
632         self.linux_xattr = result
633
634     def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
635         if not xattr:
636             if self.linux_xattr:
637                 add_error("%s: can't restore xattr; xattr support missing.\n"
638                           % path)
639             return
640         if not self.linux_xattr:
641             return
642         try:
643             existing_xattrs = set(xattr.list(path, nofollow=True))
644         except IOError, e:
645             if e.errno == errno.EACCES:
646                 raise ApplyError('xattr.set: %s' % e)
647             else:
648                 raise
649         for k, v in self.linux_xattr:
650             if k not in existing_xattrs \
651                     or v != xattr.get(path, k, nofollow=True):
652                 try:
653                     xattr.set(path, k, v, nofollow=True)
654                 except IOError, e:
655                     if e.errno == errno.EPERM \
656                             or e.errno == errno.EOPNOTSUPP:
657                         raise ApplyError('xattr.set: %s' % e)
658                     else:
659                         raise
660             existing_xattrs -= frozenset([k])
661         for k in existing_xattrs:
662             try:
663                 xattr.remove(path, k, nofollow=True)
664             except IOError, e:
665                 if e.errno == errno.EPERM:
666                     raise ApplyError('xattr.remove: %s' % e)
667                 else:
668                     raise
669
670     def __init__(self):
671         self.mode = None
672         # optional members
673         self.path = None
674         self.size = None
675         self.symlink_target = None
676         self.hardlink_target = None
677         self.linux_attr = None
678         self.linux_xattr = None
679         self.posix1e_acl = None
680
681     def write(self, port, include_path=True):
682         records = include_path and [(_rec_tag_path, self._encode_path())] or []
683         records.extend([(_rec_tag_common, self._encode_common()),
684                         (_rec_tag_symlink_target,
685                          self._encode_symlink_target()),
686                         (_rec_tag_hardlink_target,
687                          self._encode_hardlink_target()),
688                         (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
689                         (_rec_tag_linux_attr, self._encode_linux_attr()),
690                         (_rec_tag_linux_xattr, self._encode_linux_xattr())])
691         for tag, data in records:
692             if data:
693                 vint.write_vuint(port, tag)
694                 vint.write_bvec(port, data)
695         vint.write_vuint(port, _rec_tag_end)
696
697     def encode(self, include_path=True):
698         port = StringIO()
699         self.write(port, include_path)
700         return port.getvalue()
701
702     @staticmethod
703     def read(port):
704         # This method should either return a valid Metadata object,
705         # return None if there was no information at all (just a
706         # _rec_tag_end), throw EOFError if there was nothing at all to
707         # read, or throw an Exception if a valid object could not be
708         # read completely.
709         tag = vint.read_vuint(port)
710         if tag == _rec_tag_end:
711             return None
712         try: # From here on, EOF is an error.
713             result = Metadata()
714             while True: # only exit is error (exception) or _rec_tag_end
715                 if tag == _rec_tag_path:
716                     result._load_path_rec(port)
717                 elif tag == _rec_tag_common:
718                     result._load_common_rec(port)
719                 elif tag == _rec_tag_symlink_target:
720                     result._load_symlink_target_rec(port)
721                 elif tag == _rec_tag_hardlink_target:
722                     result._load_hardlink_target_rec(port)
723                 elif tag == _rec_tag_posix1e_acl:
724                     result._load_posix1e_acl_rec(port)
725                 elif tag == _rec_tag_linux_attr:
726                     result._load_linux_attr_rec(port)
727                 elif tag == _rec_tag_linux_xattr:
728                     result._load_linux_xattr_rec(port)
729                 elif tag == _rec_tag_end:
730                     return result
731                 else: # unknown record
732                     vint.skip_bvec(port)
733                 tag = vint.read_vuint(port)
734         except EOFError:
735             raise Exception("EOF while reading Metadata")
736
737     def isdir(self):
738         return stat.S_ISDIR(self.mode)
739
740     def create_path(self, path, create_symlinks=True):
741         self._create_via_common_rec(path, create_symlinks=create_symlinks)
742
743     def apply_to_path(self, path=None, restore_numeric_ids=False):
744         # apply metadata to path -- file must exist
745         if not path:
746             path = self.path
747         if not path:
748             raise Exception('Metadata.apply_to_path() called with no path')
749         if not self._recognized_file_type():
750             add_error('not applying metadata to "%s"' % path
751                       + ' with unrecognized mode "0x%x"\n' % self.mode)
752             return
753         num_ids = restore_numeric_ids
754         for apply_metadata in (self._apply_common_rec,
755                                self._apply_posix1e_acl_rec,
756                                self._apply_linux_attr_rec,
757                                self._apply_linux_xattr_rec):
758             try:
759                 apply_metadata(path, restore_numeric_ids=num_ids)
760             except ApplyError, e:
761                 add_error(e)
762
763     def same_file(self, other):
764         """Compare this to other for equivalency.  Return true if
765         their information implies they could represent the same file
766         on disk, in the hardlink sense.  Assume they're both regular
767         files."""
768         return self._same_common(other) \
769             and self._same_hardlink_target(other) \
770             and self._same_posix1e_acl(other) \
771             and self._same_linux_attr(other) \
772             and self._same_linux_xattr(other)
773
774
775 def from_path(path, statinfo=None, archive_path=None,
776               save_symlinks=True, hardlink_target=None):
777     result = Metadata()
778     result.path = archive_path
779     st = statinfo or xstat.lstat(path)
780     result.size = st.st_size
781     result._add_common(path, st)
782     if save_symlinks:
783         result._add_symlink_target(path, st)
784     result._add_hardlink_target(hardlink_target)
785     result._add_posix1e_acl(path, st)
786     result._add_linux_attr(path, st)
787     result._add_linux_xattr(path, st)
788     return result
789
790
791 def save_tree(output_file, paths,
792               recurse=False,
793               write_paths=True,
794               save_symlinks=True,
795               xdev=False):
796
797     # Issue top-level rewrite warnings.
798     for path in paths:
799         safe_path = _clean_up_path_for_archive(path)
800         if safe_path != path:
801             log('archiving "%s" as "%s"\n' % (path, safe_path))
802
803     if not recurse:
804         for p in paths:
805             safe_path = _clean_up_path_for_archive(p)
806             st = xstat.lstat(p)
807             if stat.S_ISDIR(st.st_mode):
808                 safe_path += '/'
809             m = from_path(p, statinfo=st, archive_path=safe_path,
810                           save_symlinks=save_symlinks)
811             if verbose:
812                 print >> sys.stderr, m.path
813             m.write(output_file, include_path=write_paths)
814     else:
815         start_dir = os.getcwd()
816         try:
817             for (p, st) in recursive_dirlist(paths, xdev=xdev):
818                 dirlist_dir = os.getcwd()
819                 os.chdir(start_dir)
820                 safe_path = _clean_up_path_for_archive(p)
821                 m = from_path(p, statinfo=st, archive_path=safe_path,
822                               save_symlinks=save_symlinks)
823                 if verbose:
824                     print >> sys.stderr, m.path
825                 m.write(output_file, include_path=write_paths)
826                 os.chdir(dirlist_dir)
827         finally:
828             os.chdir(start_dir)
829
830
831 def _set_up_path(meta, create_symlinks=True):
832     # Allow directories to exist as a special case -- might have
833     # been created by an earlier longer path.
834     if meta.isdir():
835         mkdirp(meta.path)
836     else:
837         parent = os.path.dirname(meta.path)
838         if parent:
839             mkdirp(parent)
840         meta.create_path(meta.path, create_symlinks=create_symlinks)
841
842
843 all_fields = frozenset(['path',
844                         'mode',
845                         'link-target',
846                         'rdev',
847                         'size',
848                         'uid',
849                         'gid',
850                         'user',
851                         'group',
852                         'atime',
853                         'mtime',
854                         'ctime',
855                         'linux-attr',
856                         'linux-xattr',
857                         'posix1e-acl'])
858
859
860 def summary_str(meta):
861     mode_val = xstat.mode_str(meta.mode)
862     user_val = meta.user
863     if not user_val:
864         user_val = str(meta.uid)
865     group_val = meta.group
866     if not group_val:
867         group_val = str(meta.gid)
868     size_or_dev_val = '-'
869     if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
870         size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
871     elif meta.size:
872         size_or_dev_val = meta.size
873     mtime_secs = xstat.fstime_floor_secs(meta.mtime)
874     time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
875     path_val = meta.path or ''
876     if stat.S_ISLNK(meta.mode):
877         path_val += ' -> ' + meta.symlink_target
878     return '%-10s %-11s %11s %16s %s' % (mode_val,
879                                          user_val + "/" + group_val,
880                                          size_or_dev_val,
881                                          time_val,
882                                          path_val)
883
884
885 def detailed_str(meta, fields = None):
886     # FIXME: should optional fields be omitted, or empty i.e. "rdev:
887     # 0", "link-target:", etc.
888     if not fields:
889         fields = all_fields
890
891     result = []
892     if 'path' in fields:
893         path = meta.path or ''
894         result.append('path: ' + path)
895     if 'mode' in fields:
896         result.append('mode: %s (%s)' % (oct(meta.mode),
897                                          xstat.mode_str(meta.mode)))
898     if 'link-target' in fields and stat.S_ISLNK(meta.mode):
899         result.append('link-target: ' + meta.symlink_target)
900     if 'rdev' in fields:
901         if meta.rdev:
902             result.append('rdev: %d,%d' % (os.major(meta.rdev),
903                                            os.minor(meta.rdev)))
904         else:
905             result.append('rdev: 0')
906     if 'size' in fields and meta.size:
907         result.append('size: ' + str(meta.size))
908     if 'uid' in fields:
909         result.append('uid: ' + str(meta.uid))
910     if 'gid' in fields:
911         result.append('gid: ' + str(meta.gid))
912     if 'user' in fields:
913         result.append('user: ' + meta.user)
914     if 'group' in fields:
915         result.append('group: ' + meta.group)
916     if 'atime' in fields:
917         # If we don't have xstat.lutime, that means we have to use
918         # utime(), and utime() has no way to set the mtime/atime of a
919         # symlink.  Thus, the mtime/atime of a symlink is meaningless,
920         # so let's not report it.  (That way scripts comparing
921         # before/after won't trigger.)
922         if xstat.lutime or not stat.S_ISLNK(meta.mode):
923             result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
924         else:
925             result.append('atime: 0')
926     if 'mtime' in fields:
927         if xstat.lutime or not stat.S_ISLNK(meta.mode):
928             result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
929         else:
930             result.append('mtime: 0')
931     if 'ctime' in fields:
932         result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
933     if 'linux-attr' in fields and meta.linux_attr:
934         result.append('linux-attr: ' + hex(meta.linux_attr))
935     if 'linux-xattr' in fields and meta.linux_xattr:
936         for name, value in meta.linux_xattr:
937             result.append('linux-xattr: %s -> %s' % (name, repr(value)))
938     if 'posix1e-acl' in fields and meta.posix1e_acl:
939         acl = meta.posix1e_acl[0]
940         result.append('posix1e-acl: ' + acl + '\n')
941         if stat.S_ISDIR(meta.mode):
942             def_acl = meta.posix1e_acl[2]
943             result.append('posix1e-acl-default: ' + def_acl + '\n')
944     return '\n'.join(result)
945
946
947 class _ArchiveIterator:
948     def next(self):
949         try:
950             return Metadata.read(self._file)
951         except EOFError:
952             raise StopIteration()
953
954     def __iter__(self):
955         return self
956
957     def __init__(self, file):
958         self._file = file
959
960
961 def display_archive(file):
962     if verbose > 1:
963         first_item = True
964         for meta in _ArchiveIterator(file):
965             if not first_item:
966                 print
967             print detailed_str(meta)
968             first_item = False
969     elif verbose > 0:
970         for meta in _ArchiveIterator(file):
971             print summary_str(meta)
972     elif verbose == 0:
973         for meta in _ArchiveIterator(file):
974             if not meta.path:
975                 print >> sys.stderr, \
976                     'bup: no metadata path, but asked to only display path', \
977                     '(increase verbosity?)'
978                 sys.exit(1)
979             print meta.path
980
981
982 def start_extract(file, create_symlinks=True):
983     for meta in _ArchiveIterator(file):
984         if not meta: # Hit end record.
985             break
986         if verbose:
987             print >> sys.stderr, meta.path
988         xpath = _clean_up_extract_path(meta.path)
989         if not xpath:
990             add_error(Exception('skipping risky path "%s"' % meta.path))
991         else:
992             meta.path = xpath
993             _set_up_path(meta, create_symlinks=create_symlinks)
994
995
996 def finish_extract(file, restore_numeric_ids=False):
997     all_dirs = []
998     for meta in _ArchiveIterator(file):
999         if not meta: # Hit end record.
1000             break
1001         xpath = _clean_up_extract_path(meta.path)
1002         if not xpath:
1003             add_error(Exception('skipping risky path "%s"' % dir.path))
1004         else:
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(path=xpath,
1011                                    restore_numeric_ids=restore_numeric_ids)
1012     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1013     for dir in all_dirs:
1014         # Don't need to check xpath -- won't be in all_dirs if not OK.
1015         xpath = _clean_up_extract_path(dir.path)
1016         if verbose:
1017             print >> sys.stderr, dir.path
1018         dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1019
1020
1021 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1022     # For now, just store all the directories and handle them last,
1023     # longest first.
1024     all_dirs = []
1025     for meta in _ArchiveIterator(file):
1026         if not meta: # Hit end record.
1027             break
1028         xpath = _clean_up_extract_path(meta.path)
1029         if not xpath:
1030             add_error(Exception('skipping risky path "%s"' % meta.path))
1031         else:
1032             meta.path = xpath
1033             if verbose:
1034                 print >> sys.stderr, '+', meta.path
1035             _set_up_path(meta, create_symlinks=create_symlinks)
1036             if os.path.isdir(meta.path):
1037                 all_dirs.append(meta)
1038             else:
1039                 if verbose:
1040                     print >> sys.stderr, '=', meta.path
1041                 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1042     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1043     for dir in all_dirs:
1044         # Don't need to check xpath -- won't be in all_dirs if not OK.
1045         xpath = _clean_up_extract_path(dir.path)
1046         if verbose:
1047             print >> sys.stderr, '=', xpath
1048         # Shouldn't have to check for risky paths here (omitted above).
1049         dir.apply_to_path(path=dir.path,
1050                           restore_numeric_ids=restore_numeric_ids)