]> arthur.barton.de Git - bup.git/blobdiff - lib/bup/helpers.py
Officially drop support for Python 2.4.
[bup.git] / lib / bup / helpers.py
index d9d177cabf30931a14e074e828204c2b7ed13999..d3eea3777676294a04c25e57e5ae0503ecd2919b 100644 (file)
@@ -1,7 +1,7 @@
 """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
 
@@ -211,16 +211,82 @@ def is_superuser():
         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
 
 
@@ -230,9 +296,10 @@ def userfullname():
     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
 
@@ -643,64 +710,97 @@ def parse_date_or_fatal(str, fatal):
         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."""