]> arthur.barton.de Git - bup.git/blob - lib/bup/metadata.py
0622d2b98652fb374f61cfba16acb00cdaa81c00
[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: %s' % 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: %s' % 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: %s' % 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, human_readable = False):
893     mode_val = xstat.mode_str(meta.mode)
894     user_val = meta.user
895     if numeric_ids or not user_val:
896         user_val = str(meta.uid)
897     group_val = meta.group
898     if numeric_ids or not group_val:
899         group_val = str(meta.gid)
900     size_or_dev_val = '-'
901     if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
902         size_or_dev_val = '%d,%d' % (os.major(meta.rdev), os.minor(meta.rdev))
903     elif meta.size != None:
904         size_or_dev_val = meta.size
905         if human_readable:
906             size_or_dev_val = format_filesize(meta.size)
907     mtime_secs = xstat.fstime_floor_secs(meta.mtime)
908     time_val = time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime_secs))
909     path_val = meta.path or ''
910     if stat.S_ISLNK(meta.mode):
911         path_val += ' -> ' + meta.symlink_target
912     return '%-10s %-11s %11s %16s %s' % (mode_val,
913                                          user_val + "/" + group_val,
914                                          size_or_dev_val,
915                                          time_val,
916                                          path_val)
917
918
919 def detailed_str(meta, fields = None):
920     # FIXME: should optional fields be omitted, or empty i.e. "rdev:
921     # 0", "link-target:", etc.
922     if not fields:
923         fields = all_fields
924
925     result = []
926     if 'path' in fields:
927         path = meta.path or ''
928         result.append('path: ' + path)
929     if 'mode' in fields:
930         result.append('mode: %s (%s)' % (oct(meta.mode),
931                                          xstat.mode_str(meta.mode)))
932     if 'link-target' in fields and stat.S_ISLNK(meta.mode):
933         result.append('link-target: ' + meta.symlink_target)
934     if 'rdev' in fields:
935         if meta.rdev:
936             result.append('rdev: %d,%d' % (os.major(meta.rdev),
937                                            os.minor(meta.rdev)))
938         else:
939             result.append('rdev: 0')
940     if 'size' in fields and meta.size:
941         result.append('size: ' + str(meta.size))
942     if 'uid' in fields:
943         result.append('uid: ' + str(meta.uid))
944     if 'gid' in fields:
945         result.append('gid: ' + str(meta.gid))
946     if 'user' in fields:
947         result.append('user: ' + meta.user)
948     if 'group' in fields:
949         result.append('group: ' + meta.group)
950     if 'atime' in fields:
951         # If we don't have xstat.lutime, that means we have to use
952         # utime(), and utime() has no way to set the mtime/atime of a
953         # symlink.  Thus, the mtime/atime of a symlink is meaningless,
954         # so let's not report it.  (That way scripts comparing
955         # before/after won't trigger.)
956         if xstat.lutime or not stat.S_ISLNK(meta.mode):
957             result.append('atime: ' + xstat.fstime_to_sec_str(meta.atime))
958         else:
959             result.append('atime: 0')
960     if 'mtime' in fields:
961         if xstat.lutime or not stat.S_ISLNK(meta.mode):
962             result.append('mtime: ' + xstat.fstime_to_sec_str(meta.mtime))
963         else:
964             result.append('mtime: 0')
965     if 'ctime' in fields:
966         result.append('ctime: ' + xstat.fstime_to_sec_str(meta.ctime))
967     if 'linux-attr' in fields and meta.linux_attr:
968         result.append('linux-attr: ' + hex(meta.linux_attr))
969     if 'linux-xattr' in fields and meta.linux_xattr:
970         for name, value in meta.linux_xattr:
971             result.append('linux-xattr: %s -> %s' % (name, repr(value)))
972     if 'posix1e-acl' in fields and meta.posix1e_acl:
973         acl = meta.posix1e_acl[0]
974         result.append('posix1e-acl: ' + acl + '\n')
975         if stat.S_ISDIR(meta.mode):
976             def_acl = meta.posix1e_acl[2]
977             result.append('posix1e-acl-default: ' + def_acl + '\n')
978     return '\n'.join(result)
979
980
981 class _ArchiveIterator:
982     def next(self):
983         try:
984             return Metadata.read(self._file)
985         except EOFError:
986             raise StopIteration()
987
988     def __iter__(self):
989         return self
990
991     def __init__(self, file):
992         self._file = file
993
994
995 def display_archive(file):
996     if verbose > 1:
997         first_item = True
998         for meta in _ArchiveIterator(file):
999             if not first_item:
1000                 print
1001             print detailed_str(meta)
1002             first_item = False
1003     elif verbose > 0:
1004         for meta in _ArchiveIterator(file):
1005             print summary_str(meta)
1006     elif verbose == 0:
1007         for meta in _ArchiveIterator(file):
1008             if not meta.path:
1009                 print >> sys.stderr, \
1010                     'bup: no metadata path, but asked to only display path', \
1011                     '(increase verbosity?)'
1012                 sys.exit(1)
1013             print meta.path
1014
1015
1016 def start_extract(file, create_symlinks=True):
1017     for meta in _ArchiveIterator(file):
1018         if not meta: # Hit end record.
1019             break
1020         if verbose:
1021             print >> sys.stderr, meta.path
1022         xpath = _clean_up_extract_path(meta.path)
1023         if not xpath:
1024             add_error(Exception('skipping risky path "%s"' % meta.path))
1025         else:
1026             meta.path = xpath
1027             _set_up_path(meta, create_symlinks=create_symlinks)
1028
1029
1030 def finish_extract(file, restore_numeric_ids=False):
1031     all_dirs = []
1032     for meta in _ArchiveIterator(file):
1033         if not meta: # Hit end record.
1034             break
1035         xpath = _clean_up_extract_path(meta.path)
1036         if not xpath:
1037             add_error(Exception('skipping risky path "%s"' % dir.path))
1038         else:
1039             if os.path.isdir(meta.path):
1040                 all_dirs.append(meta)
1041             else:
1042                 if verbose:
1043                     print >> sys.stderr, meta.path
1044                 meta.apply_to_path(path=xpath,
1045                                    restore_numeric_ids=restore_numeric_ids)
1046     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1047     for dir in all_dirs:
1048         # Don't need to check xpath -- won't be in all_dirs if not OK.
1049         xpath = _clean_up_extract_path(dir.path)
1050         if verbose:
1051             print >> sys.stderr, dir.path
1052         dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1053
1054
1055 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1056     # For now, just store all the directories and handle them last,
1057     # longest first.
1058     all_dirs = []
1059     for meta in _ArchiveIterator(file):
1060         if not meta: # Hit end record.
1061             break
1062         xpath = _clean_up_extract_path(meta.path)
1063         if not xpath:
1064             add_error(Exception('skipping risky path "%s"' % meta.path))
1065         else:
1066             meta.path = xpath
1067             if verbose:
1068                 print >> sys.stderr, '+', meta.path
1069             _set_up_path(meta, create_symlinks=create_symlinks)
1070             if os.path.isdir(meta.path):
1071                 all_dirs.append(meta)
1072             else:
1073                 if verbose:
1074                     print >> sys.stderr, '=', meta.path
1075                 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1076     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1077     for dir in all_dirs:
1078         # Don't need to check xpath -- won't be in all_dirs if not OK.
1079         xpath = _clean_up_extract_path(dir.path)
1080         if verbose:
1081             print >> sys.stderr, '=', xpath
1082         # Shouldn't have to check for risky paths here (omitted above).
1083         dir.apply_to_path(path=dir.path,
1084                           restore_numeric_ids=restore_numeric_ids)