index 6f940f79303226f2021315f84263037b48c22dae..d8adbee4198c346131b50994d5a26224c339c28b 100755 (executable)
+'''
+ Metakit backend for Roundup, originally by Gordon McMillan.
+
+ Notes by Richard:
+
+ This backend has some behaviour specific to metakit:
+
+ - there's no concept of an explicit "unset" in metakit, so all types
+ have some "unset" value:
+
+ ========= ===== ====================================================
+ Type Value Action when fetching from mk
+ ========= ===== ====================================================
+ Strings '' convert to None
+ Date 0 (seconds since 1970-01-01.00:00:00) convert to None
+ Interval '' convert to None
+ Number 0 ambiguious :( - do nothing
+ Boolean 0 ambiguious :( - do nothing
+ Link '' convert to None
+ Multilink [] actually, mk can handle this one ;)
+ Passowrd '' convert to None
+ ========= ===== ====================================================
+
+ The get/set routines handle these values accordingly by converting
+ to/from None where they can. The Number/Boolean types are not able
+ to handle an "unset" at all, so they default the "unset" to 0.
+
+ - probably a bunch of stuff that I'm not aware of yet because I haven't
+ fully read through the source. One of these days....
+'''
from roundup import hyperdb, date, password, roundupdb, security
import metakit
-from sessions import Sessions
+from sessions import Sessions, OneTimeKeys
import re, marshal, os, sys, weakref, time, calendar
from roundup import indexer
import locking
_dbs = {}
def Database(config, journaltag=None):
+ ''' Only have a single instance of the Database class for each instance
+ '''
db = _dbs.get(config.DATABASE, None)
if db is None or db._db is None:
db = _Database(config, journaltag)
pass
return db
-class _Database(hyperdb.Database):
+class _Database(hyperdb.Database, roundupdb.Database):
def __init__(self, config, journaltag=None):
self.config = config
self.journaltag = journaltag
self._db = self.__open()
self.indexer = Indexer(self.config.DATABASE, self._db)
self.sessions = Sessions(self.config)
+ self.otks = OneTimeKeys(self.config)
self.security = security.Security(self)
os.umask(0002)
if self.journaltag is None:
return None
+ # try to set the curuserid from the journaltag
try:
- self.curuserid = x = int(self.classes['user'].lookup(self.journaltag))
+ x = int(self.classes['user'].lookup(self.journaltag))
+ self.curuserid = x
except KeyError:
if self.journaltag == 'admin':
self.curuserid = x = 1
return x
elif classname == 'transactions':
return self.dirty
+ # fall back on the classes
return self.getclass(classname)
def getclass(self, classname):
try:
def getclasses(self):
return self.classes.keys()
# --- end of ping's spec
+
# --- exposed methods
def commit(self):
if self.dirty:
#usernm = userclass.get(str(row.user), 'username')
dt = date.Date(time.gmtime(row.date))
#rslt.append((nodeid, dt, usernm, _actionnames[row.action], params))
- rslt.append((nodeid, dt, str(row.user), _actionnames[row.action], params))
+ rslt.append((nodeid, dt, str(row.user), _actionnames[row.action],
+ params))
return rslt
-
+
def destroyjournal(self, tablenm, nodeid):
nodeid = int(nodeid)
tblid = self.tables.find(name=tablenm)
# --- internal
def __open(self):
+ ''' Open the metakit database
+ '''
+ # make the database dir if it doesn't exist
if not os.path.exists(self.config.DATABASE):
os.makedirs(self.config.DATABASE)
+
+ # figure the file names
self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
lockfilenm = db[:-3]+'lck'
+
+ # get the database lock
self.lockfile = locking.acquire_lock(lockfilenm)
self.lockfile.write(str(os.getpid()))
self.lockfile.flush()
+
+ # see if the schema has changed since last db access
self.fastopen = 0
if os.path.exists(db):
dbtm = os.path.getmtime(db)
else:
# can't find schemamod - must be frozen
self.fastopen = 1
+
+ # open the db
db = metakit.storage(db, 1)
hist = db.view('history')
tables = db.view('tables')
if not self.fastopen:
+ # create the database if it's brand new
if not hist.structure():
hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
if not tables.structure():
tables = db.getas('tables[name:S]')
db.commit()
+
+ # we now have an open, initialised database
self.tables = tables
self.hist = hist
return db
+
+ def setid(self, classname, maxid):
+ ''' No-op in metakit
+ '''
+ pass
_STRINGTYPE = type('')
_LISTTYPE = type([])
# --- the hyperdb.Class methods
def create(self, **propvalues):
self.fireAuditors('create', None, propvalues)
+ newid = self.create_inner(**propvalues)
+ # self.set() (called in self.create_inner()) does reactors)
+ return newid
+
+ def create_inner(self, **propvalues):
rowdict = {}
rowdict['id'] = newid = self.maxid
self.maxid += 1
if self.do_journal and prop.do_journal:
# register the unlink with the old linked node
if oldvalue:
- self.db.addjournal(link_class, value, _UNLINK,
+ self.db.addjournal(link_class, oldvalue, _UNLINK,
(self.classname, str(row.id), key))
# register the link with the newly linked node
if self.do_journal and prop.do_journal:
self.db.addjournal(link_class, id, _LINK,
(self.classname, str(row.id), key))
-
+
+ # perform the modifications on the actual property value
sv = getattr(row, key)
i = 0
while i < len(sv):
i += 1
for id in adds:
sv.append(fid=int(id))
- changes[key] = oldvalue
+
+ # figure the journal entry
+ l = []
+ if adds:
+ l.append(('+', adds))
+ if rmvd:
+ l.append(('-', rmvd))
+ if l:
+ changes[key] = tuple(l)
+ #changes[key] = oldvalue
+
if not rmvd and not adds:
del propvalues[key]
-
+
elif isinstance(prop, hyperdb.String):
if value is not None and type(value) != _STRINGTYPE:
raise TypeError, 'new property "%s" not a string'%key
try:
v = int(value)
except ValueError:
- raise TypeError, "%s (%s) is not numeric" % (key, repr(value))
+ raise TypeError, "%s (%s) is not numeric"%(key, repr(value))
setattr(row, key, v)
changes[key] = oldvalue
propvalues[key] = value
if value is None:
bv = 0
elif value not in (0,1):
- raise TypeError, "%s (%s) is not boolean" % (key, repr(value))
+ raise TypeError, "%s (%s) is not boolean"%(key, repr(value))
else:
bv = value
setattr(row, key, bv)
row.creation = int(time.time())
if not row.creator:
row.creator = self.db.curuserid
-
+
self.db.dirty = 1
if self.do_journal:
if isnew:
ndx = view.find(id=int(nodeid))
if ndx < 0:
raise KeyError, "nodeid %s not found" % nodeid
+
row = view[ndx]
oldvalues = self.uncommitted.setdefault(row.id, {})
oldval = oldvalues['_isdel'] = row._isdel
row._isdel = 1
+
if self.do_journal:
self.db.addjournal(self.classname, nodeid, _RETIRE, {})
if self.keyname:
self.db.dirty = 1
self.fireReactors('retire', nodeid, None)
+ def is_retired(self, nodeid):
+ view = self.getview(1)
+ # node must exist & not be retired
+ id = int(nodeid)
+ ndx = view.find(id=id)
+ if ndx < 0:
+ raise IndexError, "%s has no node %s" % (self.classname, nodeid)
+ row = view[ndx]
+ return row._isdel
+
def history(self, nodeid):
if not self.do_journal:
raise ValueError, 'Journalling is disabled for this class'
l.append(str(row.id))
return l
+ def getnodeids(self):
+ l = []
+ for row in self.getview():
+ l.append(str(row.id))
+ return l
+
def count(self):
return len(self.getview())
raise ValueError, "%s is already a property of %s"%(key,
self.classname)
self.ruprops.update(properties)
+ # Class structure has changed
self.db.fastopen = 0
view = self.__getview()
self.db.commit()
else:
bv = value
where[propname] = bv
+ elif isinstance(prop, hyperdb.Date):
+ t = date.Date(value).get_tuple()
+ where[propname] = int(calendar.timegm(t))
+ elif isinstance(prop, hyperdb.Interval):
+ where[propname] = str(date.Interval(value))
elif isinstance(prop, hyperdb.Number):
where[propname] = int(value)
else:
if regexes:
def ff(row, r=regexes):
for propname, regex in r.items():
- val = getattr(row, propname)
+ val = str(getattr(row, propname))
if not regex.search(val):
return 0
return 1
elif isinstance(proptype, hyperdb.Password):
value = str(value)
l.append(repr(value))
+
+ # append retired flag
+ l.append(self.is_retired(nodeid))
+
return l
def import_list(self, propnames, proplist):
view = self.getview(1)
for i in range(len(propnames)):
value = eval(proplist[i])
+ if not value:
+ continue
+
propname = propnames[i]
- prop = properties[propname]
if propname == 'id':
- newid = value
- value = int(value)
- elif isinstance(prop, hyperdb.Date):
+ newid = value = int(value)
+ elif propname == 'is retired':
+ # is the item retired?
+ if int(value):
+ d['_isdel'] = 1
+ continue
+
+ prop = properties[propname]
+ if isinstance(prop, hyperdb.Date):
value = int(calendar.timegm(value))
elif isinstance(prop, hyperdb.Interval):
value = str(date.Interval(value))
+ elif isinstance(prop, hyperdb.Number):
+ value = int(value)
+ elif isinstance(prop, hyperdb.Boolean):
+ value = int(value)
+ elif isinstance(prop, hyperdb.Link) and value:
+ value = int(value)
+ elif isinstance(prop, hyperdb.Multilink):
+ # we handle multilinks separately
+ continue
d[propname] = value
+
+ # possibly make a new node
+ if not d.has_key('id'):
+ d['id'] = newid = self.maxid
+ self.maxid += 1
+
+ # save off the node
view.append(d)
- creator = d.get('creator', None)
- creation = d.get('creation', None)
- self.db.addjournal(self.classname, newid, 'create', {}, creator,
+
+ # fix up multilinks
+ ndx = view.find(id=newid)
+ row = view[ndx]
+ for i in range(len(propnames)):
+ value = eval(proplist[i])
+ propname = propnames[i]
+ if propname == 'is retired':
+ continue
+ prop = properties[propname]
+ if not isinstance(prop, hyperdb.Multilink):
+ continue
+ sv = getattr(row, propname)
+ for entry in value:
+ sv.append(int(entry))
+
+ self.db.dirty = 1
+ creator = d.get('creator', 0)
+ creation = d.get('creation', 0)
+ self.db.addjournal(self.classname, str(newid), _CREATE, {}, creator,
creation)
return newid
self.rbactions.append(action)
# --- internal
def __getview(self):
+ ''' Find the interface for a specific Class in the hyperdb.
+
+ This method checks to see whether the schema has changed and
+ re-works the underlying metakit structure if it has.
+ '''
db = self.db._db
view = db.view(self.classname)
mkprops = view.structure()
+
+ # if we have structure in the database, and the structure hasn't
+ # changed
if mkprops and self.db.fastopen:
return view.ordered(1)
+
# is the definition the same?
for nm, rutyp in self.ruprops.items():
for mkprop in mkprops:
return self.db._db.view(self.classname).ordered(1)
def getindexview(self, RW=0):
return self.db._db.view("_%s" % self.classname).ordered(1)
-
+
def _fetchML(sv):
l = []
for row in sv:
return l
def _fetchPW(s):
+ ''' Convert to a password.Password unless the password is '' which is
+ our sentinel for "unset".
+ '''
+ if s == '':
+ return None
p = password.Password()
p.unpack(s)
return p
def _fetchLink(n):
+ ''' Return None if the link is 0 - otherwise strify it.
+ '''
return n and str(n) or None
def _fetchDate(n):
+ ''' Convert the timestamp to a date.Date instance - unless it's 0 which
+ is our sentinel for "unset".
+ '''
+ if n == 0:
+ return None
return date.Date(time.gmtime(n))
+def _fetchInterval(n):
+ ''' Convert to a date.Interval unless the interval is '' which is our
+ sentinel for "unset".
+ '''
+ if n == '':
+ return None
+ return date.Interval(n)
+
_converters = {
hyperdb.Date : _fetchDate,
hyperdb.Link : _fetchLink,
hyperdb.Multilink : _fetchML,
- hyperdb.Interval : date.Interval,
+ hyperdb.Interval : _fetchInterval,
hyperdb.Password : _fetchPW,
hyperdb.Boolean : lambda n: n,
hyperdb.Number : lambda n: n,
- hyperdb.String : str,
+ hyperdb.String : lambda s: s and str(s) or None,
}
class FileName(hyperdb.String):
hyperdb.Boolean : 'I',
hyperdb.Number : 'I',
}
-class FileClass(Class):
+class FileClass(Class, hyperdb.FileClass):
''' like Class but with a content property
'''
default_mime_type = 'text/plain'
def get(self, nodeid, propname, default=_marker, cache=1):
x = Class.get(self, nodeid, propname, default, cache)
+ poss_msg = 'Possibly an access right configuration problem.'
if propname == 'content':
if x.startswith('file:'):
fnm = x[5:]
try:
x = open(fnm, 'rb').read()
- except Exception, e:
- x = repr(e)
+ except IOError, (strerror):
+ # XXX by catching this we donot see an error in the log.
+ return 'ERROR reading file: %s%s\n%s\n%s'%(
+ self.classname, nodeid, poss_msg, strerror)
return x
def create(self, **propvalues):
+ self.fireAuditors('create', None, propvalues)
content = propvalues['content']
del propvalues['content']
- newid = Class.create(self, **propvalues)
+ newid = Class.create_inner(self, **propvalues)
if not content:
return newid
nm = bnm = '%s%s' % (self.classname, newid)