chiark / gitweb /
Found in crybaby's working tree.
[chopwood] / backend.py
index 576486cb4f4ac1e8f41cd3fe83c60670758f060d..2596306363e7c6171107bdbfa8d26b81de8f40c9 100644 (file)
@@ -25,6 +25,8 @@
 
 from __future__ import with_statement
 
+from auto import HOME
+import errno as E
 import itertools as I
 import os as OS; ENV = OS.environ
 
@@ -42,7 +44,8 @@ CONF.DEFAULTS.update(
 ###--------------------------------------------------------------------------
 ### Utilities.
 
-def fill_in_fields(fno_user, fno_passwd, fno_map, user, passwd, args):
+def fill_in_fields(fno_user, fno_passwd, fno_map,
+                   user, passwd, args, defaults = None):
   """
   Return a vector of filled-in fields.
 
@@ -51,6 +54,10 @@ def fill_in_fields(fno_user, fno_passwd, fno_map, user, passwd, args):
   is a sequence of (NAME, POS) pairs.  The USER and PASSWD arguments give the
   actual user name and password values; ARGS are the remaining arguments,
   maybe in the form `NAME=VALUE'.
+
+  If DEFAULTS is given, it is a fully-filled-in sequence of records.  The
+  user name and password are taken from the DEFAULTS if USER and PASSWD are
+  `None' on entry; they cannot be set from ARGS.
   """
 
   ## Prepare the result vector, and set up some data structures.
@@ -61,7 +68,8 @@ def fill_in_fields(fno_user, fno_passwd, fno_map, user, passwd, args):
   if fno_user >= n or fno_passwd >= n: ok = False
   for k, i in fno_map:
     fmap[k] = i
-    rmap[i] = "`%s'" % k
+    if i in rmap: ok = False
+    else: rmap[i] = "`%s'" % k
     if i >= n: ok = False
   if not ok:
     raise U.ExpectedError, \
@@ -69,8 +77,12 @@ def fill_in_fields(fno_user, fno_passwd, fno_map, user, passwd, args):
 
   ## Prepare the new record's fields.
   f = [None]*n
-  f[fno_user] = user
-  f[fno_passwd] = passwd
+  if user is not None: f[fno_user] = user
+  elif defaults is not None: f[no_user] = defaults[no_user]
+  else: raise U.ExpectedError, (500, "No user name given")
+  if passwd is not None: f[fno_passwd] = passwd
+  elif defaults is not None: f[no_passwd] = defaults[no_passwd]
+  else: raise U.ExpectedError, (500, "No password given")
 
   for a in args:
     if '=' in a:
@@ -87,10 +99,12 @@ def fill_in_fields(fno_user, fno_passwd, fno_map, user, passwd, args):
       raise U.ExpectedError, (400, "Field %s is already set" % rmap[i])
     f[i] = v
 
-  ## Check that the vector of fields is properly set up.
+  ## Check that the vector of fields is properly set up.  Copy unset values
+  ## from the defaults if they're available.
   for i in xrange(n):
-    if f[i] is None:
-      raise U.ExpectedError, (500, "Field %s is unset" % rmap[i])
+    if f[i] is not None: pass
+    elif defaults is not None: f[i] = defaults[i]
+    else: raise U.ExpectedError, (500, "Field %s is unset" % rmap[i])
 
   ## Done.
   return f
@@ -163,7 +177,7 @@ class FlatFileRecord (BasicRecord):
   is careful to do this).
   """
 
-  def __init__(me, line, delim, fmap, *args, **kw):
+  def __init__(me, line, delim, fno_user, fno_passwd, fmap, *args, **kw):
     """
     Initialize the record, splitting the LINE into fields separated by DELIM,
     and setting attributes under control of FMAP.
@@ -172,6 +186,8 @@ class FlatFileRecord (BasicRecord):
     line = line.rstrip('\n')
     fields = line.split(delim)
     me._delim = delim
+    me._fno_user = fno_user
+    me._fno_passwd = fno_passwd
     me._fmap = fmap
     me._raw = fields
     for k, v in fmap.iteritems():
@@ -197,6 +213,14 @@ class FlatFileRecord (BasicRecord):
       fields[v] = val
     return me._delim.join(fields) + '\n'
 
+  def edit(me, args):
+    """
+    Modify the record as described by the ARGS.
+
+    Fields other than the user name and password can be modified.
+    """
+    ff = fill_in_fields(
+
 class FlatFileBackend (object):
   """
   Password storage in a flat passwd(5)-style file.
@@ -206,10 +230,13 @@ class FlatFileBackend (object):
   specified by the DELIM constructor argument.
 
   The file is updated by writing a new version alongside, as `FILE.new', and
-  renaming it over the old version.  If a LOCK file is named then an
-  exclusive fcntl(2)-style lock is taken out on `LOCKDIR/LOCK' (creating the
-  file if necessary) during the update operation.  Use of a lockfile is
-  strongly recommended.
+  renaming it over the old version.  If a LOCK is provided then this is done
+  while holding a lock.  By default, an exclusive fcntl(2)-style lock is
+  taken out on `LOCKDIR/LOCK' (creating the file if necessary) during the
+  update operation, but subclasses can override the `dolocked' method to
+  provide alternative locking behaviour; the LOCK parameter is not
+  interpreted by any other methods.  Use of a lockfile is strongly
+  recommended.
 
   The DELIM constructor argument specifies the delimiter character used when
   splitting lines into fields.  The USER and PASSWD arguments give the field
@@ -329,11 +356,21 @@ class FlatFileBackend (object):
 
     ## If there's a lockfile, then acquire it around the meat of this
     ## function; otherwise just do the job.
-    if me._lock is None:
-      doit()
-    else:
-      with U.lockfile(OS.path.join(CFG.LOCKDIR, me._lock), 5):
-        doit()
+    if me._lock is None: doit()
+    else: me.dolocked(me._lock, doit)
+
+  def dolocked(me, lock, func):
+    """
+    Call FUNC with the LOCK held.
+
+    Subclasses can override this method in order to provide alternative
+    locking functionality.
+    """
+    try: OS.mkdir(CFG.LOCKDIR)
+    except OSError, e:
+      if e.errno != E.EEXIST: raise
+    with U.lockfile(OS.path.join(CFG.LOCKDIR, lock), 5):
+      func()
 
   def _parse(me, line):
     """Convenience function for constructing a record."""