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