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