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