+###--------------------------------------------------------------------------
+### Backend storage.
+
+class StorageBackend (object):
+ """
+ I provide a backend for password and metadata storage.
+
+ Backends are responsible for storing and retrieving stuff, but not for the
+ cryptographic details. Backends need to store two kinds of information:
+
+ * metadata, consisting of a number of property names and their values;
+ and
+
+ * password mappings, consisting of a number of binary labels and
+ payloads.
+ """
+
+ FAIL = ['FAIL']
+
+ ## Life cycle methods.
+
+ @classmethod
+ def create(cls, file):
+ """Create a new database in the named FILE, using this backend."""
+ return cls(writep = True, _magic = lambda me: me._create(file))
+ def _create(me, file):
+ me._db = _G.open(file, 'n', 0600)
+
+ def __init__(me, file = None, writep = False, _magic = None, *args, **kw):
+ """
+ Main constructor.
+ """
+ super(StorageBackend, me).__init__(*args, **kw)
+ if _magic is not None: _magic(me)
+ elif file is None: raise ValueError, 'missing file parameter'
+ else: me._db = _G.open(file, writep and 'w' or 'r')
+ me._writep = writep
+ me._livep = True
+
+ def close(me):
+ """
+ Close the database.
+
+ It is harmless to attempt to close a database which has been closed
+ already.
+ """
+ if me._livep:
+ me._livep = False
+ me._db.close()
+
+ ## Utilities.
+
+ def _check_live(me):
+ """Raise an error if the receiver has been closed."""
+ if not me._livep: raise ValueError, 'database is closed'
+
+ def _check_write(me):
+ """Raise an error if the receiver is not open for writing."""
+ me._check_live()
+ if not me._writep: raise ValueError, 'database is read-only'
+
+ def _check_meta_name(me, name):
+ """
+ Raise an error unless NAME is a valid name for a metadata item.
+
+ Metadata names may not start with `$': such names are reserved for
+ password storage.
+ """
+ if name.startswith('$'):
+ raise ValueError, "invalid metadata key `%s'" % name
+
+ ## Context protocol.
+
+ def __enter__(me):
+ """Context protocol: make sure the database is closed on exit."""
+ return me
+ def __exit__(me, exctype, excvalue, exctb):
+ """Context protocol: see `__enter__'."""
+ me.close()
+
+ ## Metadata.
+
+ def get_meta(me, name, default = FAIL):
+ """
+ Fetch the value for the metadata item NAME.
+
+ If no such item exists, then return DEFAULT if that was set; otherwise
+ raise a `KeyError'.
+ """
+ me._check_meta_name(name)
+ me._check_live()
+ try: value = me._db[name]
+ except KeyError: value = default
+ if value is StorageBackend.FAIL: raise KeyError, name
+ return value
+
+ def put_meta(me, name, value):
+ """Store VALUE in the metadata item called NAME."""
+ me._check_meta_name(name)
+ me._check_write()
+ me._db[name] = value
+
+ def del_meta(me, name):
+ """Forget about the metadata item with the given NAME."""
+ me._check_meta_name(name)
+ me._check_write()
+ del me._db[name]
+
+ def iter_meta(me):
+ """Return an iterator over the name/value metadata items."""
+ me._check_live()
+ k = me._db.firstkey()
+ while k is not None:
+ if not k.startswith('$'): yield k, me._db[k]
+ k = me._db.nextkey(k)
+
+ ## Passwords.
+
+ def get_passwd(me, label):
+ """
+ Fetch and return the payload stored with the (opaque, binary) LABEL.
+
+ If there is no such payload then raise `KeyError'.
+ """
+ me._check_live()
+ return me._db['$' + label]
+
+ def put_passwd(me, label, payload):
+ """
+ Associate the (opaque, binary) PAYLOAD with the (opaque, binary) LABEL.
+
+ Any previous payload for LABEL is forgotten.
+ """
+ me._check_write()
+ me._db['$' + label] = payload
+
+ def del_passwd(me, label):
+ """
+ Forget any PAYLOAD associated with the (opaque, binary) LABEL.
+
+ If there is no such payload then raise `KeyError'.
+ """
+ me._check_write()
+ del me._db['$' + label]
+
+ def iter_passwds(me):
+ """Return an iterator over the stored password label/payload pairs."""
+ me._check_live()
+ k = me._db.firstkey()
+ while k is not None:
+ if k.startswith('$'): yield k[1:], me._db[k]
+ k = me._db.nextkey(k)
+