"""Helper functions and classes for bup."""
import sys, os, pwd, subprocess, errno, socket, select, mmap, stat, re, struct
-import heapq, operator, time, platform
+import hashlib, heapq, operator, time, platform, grp
from bup import _version, _helpers
import bup._helpers as _helpers
return os.geteuid() == 0
+def _cache_key_value(get_value, key, cache):
+ """Return (value, was_cached). If there is a value in the cache
+ for key, use that, otherwise, call get_value(key) which should
+ throw a KeyError if there is no value -- in which case the cached
+ and returned value will be None.
+ """
+ try: # Do we already have it (or know there wasn't one)?
+ value = cache[key]
+ return value, True
+ except KeyError:
+ pass
+ value = None
+ try:
+ cache[key] = value = get_value(key)
+ except KeyError:
+ cache[key] = None
+ return value, False
+
+
+_uid_to_pwd_cache = {}
+_name_to_pwd_cache = {}
+
+def pwd_from_uid(uid):
+ """Return password database entry for uid (may be a cached value).
+ Return None if no entry is found.
+ """
+ global _uid_to_pwd_cache, _name_to_pwd_cache
+ entry, cached = _cache_key_value(pwd.getpwuid, uid, _uid_to_pwd_cache)
+ if entry and not cached:
+ _name_to_pwd_cache[entry.pw_name] = entry
+ return entry
+
+
+def pwd_from_name(name):
+ """Return password database entry for name (may be a cached value).
+ Return None if no entry is found.
+ """
+ global _uid_to_pwd_cache, _name_to_pwd_cache
+ entry, cached = _cache_key_value(pwd.getpwnam, name, _name_to_pwd_cache)
+ if entry and not cached:
+ _uid_to_pwd_cache[entry.pw_uid] = entry
+ return entry
+
+
+_gid_to_grp_cache = {}
+_name_to_grp_cache = {}
+
+def grp_from_gid(gid):
+ """Return password database entry for gid (may be a cached value).
+ Return None if no entry is found.
+ """
+ global _gid_to_grp_cache, _name_to_grp_cache
+ entry, cached = _cache_key_value(grp.getgrgid, gid, _gid_to_grp_cache)
+ if entry and not cached:
+ _name_to_grp_cache[entry.gr_name] = entry
+ return entry
+
+
+def grp_from_name(name):
+ """Return password database entry for name (may be a cached value).
+ Return None if no entry is found.
+ """
+ global _gid_to_grp_cache, _name_to_grp_cache
+ entry, cached = _cache_key_value(grp.getgrnam, name, _name_to_grp_cache)
+ if entry and not cached:
+ _gid_to_grp_cache[entry.gr_gid] = entry
+ return entry
+
+
_username = None
def username():
"""Get the user's login name."""
global _username
if not _username:
uid = os.getuid()
- try:
- _username = pwd.getpwuid(uid)[0]
- except KeyError:
- _username = 'user%d' % uid
+ _username = pwd_from_uid(uid)[0] or 'user%d' % uid
return _username
global _userfullname
if not _userfullname:
uid = os.getuid()
- try:
- _userfullname = pwd.getpwuid(uid)[4].split(',')[0]
- except KeyError:
+ entry = pwd_from_uid(uid)
+ if entry:
+ _userfullname = entry[4].split(',')[0] or entry[0]
+ if not _userfullname:
_userfullname = 'user%d' % uid
return _userfullname
return date
-def strip_path(prefix, path):
- """Strips a given prefix from a path.
-
- First both paths are normalized.
-
- Raises an Exception if no prefix is given.
- """
- if prefix == None:
- raise Exception('no path given')
-
- normalized_prefix = os.path.realpath(prefix)
- debug2("normalized_prefix: %s\n" % normalized_prefix)
- normalized_path = os.path.realpath(path)
- debug2("normalized_path: %s\n" % normalized_path)
- if normalized_path.startswith(normalized_prefix):
- return normalized_path[len(normalized_prefix):]
- else:
- return path
-
-
-def strip_base_path(path, base_paths):
- """Strips the base path from a given path.
-
-
- Determines the base path for the given string and then strips it
- using strip_path().
- Iterates over all base_paths from long to short, to prevent that
- a too short base_path is removed.
- """
- normalized_path = os.path.realpath(path)
- sorted_base_paths = sorted(base_paths, key=len, reverse=True)
- for bp in sorted_base_paths:
- if normalized_path.startswith(os.path.realpath(bp)):
- return strip_path(bp, normalized_path)
- return path
-
-
-def graft_path(graft_points, path):
- normalized_path = os.path.realpath(path)
+# FIXME: Carefully consider the use of functions (os.path.*, etc.)
+# that resolve against the current filesystem in the strip/graft
+# functions for example, but elsewhere as well. I suspect bup's not
+# always being careful about that. For some cases, the contents of
+# the current filesystem should be irrelevant, and consulting it might
+# produce the wrong result, perhaps via unintended symlink resolution,
+# for example.
+
+def path_components(path):
+ """Break path into a list of pairs of the form (name,
+ full_path_to_name). Path must start with '/'.
+ Example:
+ '/home/foo' -> [('', '/'), ('home', '/home'), ('foo', '/home/foo')]"""
+ assert(path.startswith('/'))
+ # Since we assume path startswith('/'), we can skip the first element.
+ result = [('', '/')]
+ norm_path = os.path.abspath(path)
+ if norm_path == '/':
+ return result
+ full_path = ''
+ for p in norm_path.split('/')[1:]:
+ full_path += '/' + p
+ result.append((p, full_path))
+ return result
+
+
+def stripped_path_components(path, strip_prefixes):
+ """Strip any prefix in strip_prefixes from path and return a list
+ of path components where each component is (name,
+ none_or_full_fs_path_to_name). Assume path startswith('/').
+ See thelpers.py for examples."""
+ normalized_path = os.path.abspath(path)
+ sorted_strip_prefixes = sorted(strip_prefixes, key=len, reverse=True)
+ for bp in sorted_strip_prefixes:
+ normalized_bp = os.path.abspath(bp)
+ if normalized_path.startswith(normalized_bp):
+ prefix = normalized_path[:len(normalized_bp)]
+ result = []
+ for p in normalized_path[len(normalized_bp):].split('/'):
+ if p: # not root
+ prefix += '/'
+ prefix += p
+ result.append((p, prefix))
+ return result
+ # Nothing to strip.
+ return path_components(path)
+
+
+def grafted_path_components(graft_points, path):
+ # Create a result that consists of some number of faked graft
+ # directories before the graft point, followed by all of the real
+ # directories from path that are after the graft point. Arrange
+ # for the directory at the graft point in the result to correspond
+ # to the "orig" directory in --graft orig=new. See t/thelpers.py
+ # for some examples.
+
+ # Note that given --graft orig=new, orig and new have *nothing* to
+ # do with each other, even if some of their component names
+ # match. i.e. --graft /foo/bar/baz=/foo/bar/bax is semantically
+ # equivalent to --graft /foo/bar/baz=/x/y/z, or even
+ # /foo/bar/baz=/x.
+
+ # FIXME: This can't be the best solution...
+ clean_path = os.path.abspath(path)
for graft_point in graft_points:
old_prefix, new_prefix = graft_point
- if normalized_path.startswith(old_prefix):
- return re.sub(r'^' + old_prefix, new_prefix, normalized_path)
- return normalized_path
-
-
-# hashlib is only available in python 2.5 or higher, but the 'sha' module
-# produces a DeprecationWarning in python 2.6 or higher. We want to support
-# python 2.4 and above without any stupid warnings, so let's try using hashlib
-# first, and downgrade if it fails.
-try:
- import hashlib
-except ImportError:
- import sha
- Sha1 = sha.sha
-else:
- Sha1 = hashlib.sha1
-
+ # Expand prefixes iff not absolute paths.
+ old_prefix = os.path.normpath(old_prefix)
+ new_prefix = os.path.normpath(new_prefix)
+ if clean_path.startswith(old_prefix):
+ escaped_prefix = re.escape(old_prefix)
+ grafted_path = re.sub(r'^' + escaped_prefix, new_prefix, clean_path)
+ # Handle /foo=/ (at least) -- which produces //whatever.
+ grafted_path = '/' + grafted_path.lstrip('/')
+ clean_path_components = path_components(clean_path)
+ # Count the components that were stripped.
+ strip_count = 0 if old_prefix == '/' else old_prefix.count('/')
+ new_prefix_parts = new_prefix.split('/')
+ result_prefix = grafted_path.split('/')[:new_prefix.count('/')]
+ result = [(p, None) for p in result_prefix] \
+ + clean_path_components[strip_count:]
+ # Now set the graft point name to match the end of new_prefix.
+ graft_point = len(result_prefix)
+ result[graft_point] = \
+ (new_prefix_parts[-1], clean_path_components[strip_count][1])
+ if new_prefix == '/': # --graft ...=/ is a special case.
+ return result[1:]
+ return result
+ return path_components(clean_path)
+
+Sha1 = hashlib.sha1
def version_date():
"""Format bup's version date string for output."""