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