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