chiark / gitweb /
catacomb/pwsafe.py: Add a new ABRUPTP argument to `close' methods.
[catacomb-python] / catacomb / pwsafe.py
index 67b249e747bd11ef1304287501010a915ac1e329..8b68182e098b1df3231bfa03f435fb0829bea48e 100644 (file)
@@ -28,6 +28,8 @@
 
 from __future__ import with_statement
 
+import os as _OS
+
 import catacomb as _C
 import gdbm as _G
 
@@ -128,12 +130,35 @@ class PPK (Crypto):
 ###--------------------------------------------------------------------------
 ### Backend storage.
 
+class StorageBackendRefusal (Exception):
+  """
+  I signify that a StorageBackend subclass has refused to open a file.
+
+  This is used by the StorageBackend.open class method.
+  """
+  pass
+
+class StorageBackendClass (type):
+  """
+  I am a metaclass for StorageBackend classes.
+
+  My main feature is that I register my concrete instances (with a `NAME'
+  which is not `None') with the StorageBackend class.
+  """
+  def __init__(me, name, supers, dict):
+    """
+    Register a new concrete StorageBackend subclass.
+    """
+    super(StorageBackendClass, me).__init__(name, supers, dict)
+    if me.NAME is not None: StorageBackend.register_concrete_subclass(me)
+
 class StorageBackend (object):
   """
   I provide basic protocol for password storage backends.
 
   I'm an abstract class: you want one of my subclasses if you actually want
-  to do something useful.
+  to do something useful.  But I maintain a list of my subclasses and can
+  choose an appropriate one to open a database file you've found lying about.
 
   Backends are responsible for storing and retrieving stuff, but not for the
   cryptographic details.  Backends need to store two kinds of information:
@@ -152,7 +177,8 @@ class StorageBackend (object):
   BE._open(FILE, WRITEP)
                         Open the existing database FILE; used by `open'.
 
-  BE._close()           Close the database, freeing up any resources.
+  BE._close(ABRUPTP)    Close the database, freeing up any resources.  If
+                        ABRUPTP then don't try to commit changes.
 
   BE._get_meta(NAME, DEFAULT)
                         Return the value of the metadata item with the given
@@ -184,11 +210,53 @@ class StorageBackend (object):
 
   BE._iter_passwds()    Return an iterator over the password (LABEL, PAYLOAD)
                         pairs; used by `iter_passwds'.
+
+  Also, concrete subclasses should define the following class attributes.
+
+  NAME                  The name of the backend, so that the user can select
+                        it when creating a new database.
+
+  PRIO                  An integer priority: backends are tried in decreasing
+                        priority order when opening an existing database.
   """
 
+  __metaclass__ = StorageBackendClass
+  NAME = None
+  PRIO = 10
+
+  ## The registry of subclasses.
+  CLASSES = {}
+
   FAIL = ['FAIL']
 
-  ## Life cycle methods.
+  @staticmethod
+  def register_concrete_subclass(sub):
+    """Register a concrete subclass, so that `open' can try it."""
+    StorageBackend.CLASSES[sub.NAME] = sub
+
+  @staticmethod
+  def byname(name):
+    """
+    Return the concrete subclass with the given NAME.
+
+    Raise `KeyError' if the name isn't found.
+    """
+    return StorageBackend.CLASSES[name]
+
+  @staticmethod
+  def classes():
+    """Return an iterator over the concrete subclasses."""
+    return StorageBackend.CLASSES.itervalues()
+
+  @staticmethod
+  def open(file, writep = False):
+    """Open a database FILE, using some appropriate backend."""
+    _OS.stat(file)
+    for cls in sorted(StorageBackend.CLASSES.values(), reverse = True,
+                      key = lambda cls: cls.PRIO):
+      try: return cls(file, writep)
+      except StorageBackendRefusal: pass
+    raise StorageBackendRefusal
 
   @classmethod
   def create(cls, file):
@@ -209,13 +277,14 @@ class StorageBackend (object):
     `_open' method, which is invoked in the usual case.
     """
     super(StorageBackend, me).__init__(*args, **kw)
+    if me.NAME is None: raise ValueError, 'abstract class'
     if _magic is not None: _magic(me)
     elif file is None: raise ValueError, 'missing file parameter'
     else: me._open(file, writep)
     me._writep = writep
     me._livep = True
 
-  def close(me):
+  def close(me, abruptp = False):
     """
     Close the database.
 
@@ -224,7 +293,7 @@ class StorageBackend (object):
     """
     if me._livep:
       me._livep = False
-      me._close()
+      me._close(abruptp)
 
   ## Utilities.
 
@@ -254,7 +323,7 @@ class StorageBackend (object):
     return me
   def __exit__(me, exctype, excvalue, exctb):
     """Context protocol: see `__enter__'."""
-    me.close()
+    me.close(excvalue is not None)
 
   ## Metadata.
 
@@ -363,6 +432,8 @@ class GDBMStorageBackend (StorageBackend):
   metadata item name begins with `$'.
   """
 
+  NAME = 'gdbm'
+
   def _open(me, file, writep):
     try: me._db = _G.open(file, writep and 'w' or 'r')
     except _G.error, e: raise StorageBackendRefusal, e
@@ -370,7 +441,7 @@ class GDBMStorageBackend (StorageBackend):
   def _create(me, file):
     me._db = _G.open(file, 'n', 0600)
 
-  def _close(me):
+  def _close(me, abruptp):
     me._db.close()
     me._db = None
 
@@ -453,7 +524,7 @@ class PW (object):
     """
 
     ## Open the database.
-    me.db = GDBMStorageBackend(file, writep)
+    me.db = StorageBackend.open(file, writep)
 
     ## Find out what crypto to use.
     c = _C.gcciphers[me.db.get_meta('cipher')]
@@ -477,9 +548,9 @@ class PW (object):
     me.magic = me.k.decrypt(me.db.get_meta('magic'))
 
   @classmethod
-  def create(cls, file, tag, c, h, m):
+  def create(cls, dbcls, file, tag, c, h, m):
     """
-    Create and initialize a new database FILE.
+    Create and initialize a new database FILE using StorageBackend DBCLS.
 
     We want a GCipher subclass C, a GHash subclass H, and a GMAC subclass M;
     and a Pixie passphrase TAG.
@@ -497,7 +568,7 @@ class PW (object):
 
     ## Set up and initialize the database.
     kct = ppk.encrypt(_C.WriteBuffer().putblk16(ck).putblk16(mk))
-    with GDBM.StorageBackend.create(file) as db:
+    with dbcls.create(file) as db:
       db.put_meta('tag', tag)
       db.put_meta('salt', ppk.salt)
       db.put_meta('cipher', c.name)
@@ -569,6 +640,6 @@ class PW (object):
   def __enter__(me):
     return me
   def __exit__(me, excty, excval, exctb):
-    me.db.close()
+    me.db.close(excval is not None)
 
 ###----- That's all, folks --------------------------------------------------