Code

fixed export/import of retired nodes (sf bug 685273)
[roundup.git] / roundup / backends / back_anydbm.py
index bad6989b53e9d7732782dd8b36c99a661f3d7160..dfacf2f48465a0aa76ab3800d683ff81e8194cb5 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-#$Id: back_anydbm.py,v 1.91 2002-11-06 05:39:49 richard Exp $
+#$Id: back_anydbm.py,v 1.106 2003-02-26 23:42:50 richard Exp $
 '''
 This module defines a backend that saves the hyperdatabase in a database
 chosen by anydbm. It is guaranteed to always be available in python
@@ -26,11 +26,11 @@ serious bugs, and is not available)
 import whichdb, anydbm, os, marshal, re, weakref, string, copy
 from roundup import hyperdb, date, password, roundupdb, security
 from blobfiles import FileStorage
-from sessions import Sessions
+from sessions import Sessions, OneTimeKeys
 from roundup.indexer import Indexer
-from locking import acquire_lock, release_lock
+from roundup.backends import locking
 from roundup.hyperdb import String, Password, Date, Interval, Link, \
-    Multilink, DatabaseError, Boolean, Number
+    Multilink, DatabaseError, Boolean, Number, Node
 
 #
 # Now the database
@@ -68,10 +68,17 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         self.transactions = []
         self.indexer = Indexer(self.dir)
         self.sessions = Sessions(self.config)
+        self.otks = OneTimeKeys(self.config)
         self.security = security.Security(self)
         # ensure files are group readable and writable
         os.umask(0002)
 
+        # lock it
+        lockfilenm = os.path.join(self.dir, 'lock')
+        self.lockfile = locking.acquire_lock(lockfilenm)
+        self.lockfile.write(str(os.getpid()))
+        self.lockfile.flush()
+
     def post_init(self):
         ''' Called once the schema initialisation has finished.
         '''
@@ -203,12 +210,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
                 mode)
         return dbm.open(path, mode)
 
-    def lockdb(self, name):
-        ''' Lock a database file
-        '''
-        path = os.path.join(os.getcwd(), self.dir, '%s.lock'%name)
-        return acquire_lock(path)
-
     #
     # Node IDs
     #
@@ -216,7 +217,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         ''' Generate a new id for the given class
         '''
         # open the ids DB - create if if doesn't exist
-        lock = self.lockdb('_ids')
         db = self.opendb('_ids', 'c')
         if db.has_key(classname):
             newid = db[classname] = str(int(db[classname]) + 1)
@@ -225,18 +225,15 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             newid = str(self.getclass(classname).count()+1)
             db[classname] = newid
         db.close()
-        release_lock(lock)
         return newid
 
     def setid(self, classname, setid):
         ''' Set the id counter: used during import of database
         '''
         # open the ids DB - create if if doesn't exist
-        lock = self.lockdb('_ids')
         db = self.opendb('_ids', 'c')
         db[classname] = str(setid)
         db.close()
-        release_lock(lock)
 
     #
     # Nodes
@@ -303,6 +300,13 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         if db is None:
             db = self.getclassdb(classname)
         if not db.has_key(nodeid):
+            # try the cache - might be a brand-new node
+            cache_dict = self.cache.setdefault(classname, {})
+            if cache_dict.has_key(nodeid):
+                if __debug__:
+                    print >>hyperdb.TRACE, 'get %s %s cached'%(classname,
+                        nodeid)
+                return cache_dict[nodeid]
             raise IndexError, "no such %s %s"%(classname, nodeid)
 
         # check the uncommitted, destroyed nodes
@@ -559,7 +563,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         '''
         if __debug__:
             print >>hyperdb.DEBUG, 'commit', (self,)
-        # TODO: lock the DB
 
         # keep a handle to all the database files opened
         self.databases = {}
@@ -573,7 +576,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         for db in self.databases.values():
             db.close()
         del self.databases
-        # TODO: unlock the DB
 
         # reindex the nodes that request it
         for classname, nodeid in filter(None, reindex.keys()):
@@ -696,7 +698,11 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     def close(self):
         ''' Nothing to do
         '''
-        pass
+        if self.lockfile is not None:
+            locking.release_lock(self.lockfile)
+        if self.lockfile is not None:
+            self.lockfile.close()
+            self.lockfile = None
 
 _marker = []
 class Class(hyperdb.Class):
@@ -760,6 +766,14 @@ class Class(hyperdb.Class):
         These operations trigger detectors and can be vetoed.  Attempts
         to modify the "creation" or "activity" properties cause a KeyError.
         '''
+        self.fireAuditors('create', None, propvalues)
+        newid = self.create_inner(**propvalues)
+        self.fireReactors('create', newid, None)
+        return newid
+
+    def create_inner(self, **propvalues):
+        ''' Called by create, in-between the audit and react calls.
+        '''
         if propvalues.has_key('id'):
             raise KeyError, '"id" is reserved'
 
@@ -768,9 +782,6 @@ class Class(hyperdb.Class):
 
         if propvalues.has_key('creation') or propvalues.has_key('activity'):
             raise KeyError, '"creation" and "activity" are reserved'
-
-        self.fireAuditors('create', None, propvalues)
-
         # new node's id
         newid = self.db.newid(self.classname)
 
@@ -847,7 +858,7 @@ class Class(hyperdb.Class):
                             (self.classname, newid, key))
 
             elif isinstance(prop, String):
-                if type(value) != type(''):
+                if type(value) != type('') and type(value) != type(u''):
                     raise TypeError, 'new property "%s" not a string'%key
 
             elif isinstance(prop, Password):
@@ -888,9 +899,7 @@ class Class(hyperdb.Class):
         # done
         self.db.addnode(self.classname, newid, propvalues)
         if self.do_journal:
-            self.db.addjournal(self.classname, newid, 'create', propvalues)
-
-        self.fireReactors('create', newid, None)
+            self.db.addjournal(self.classname, newid, 'create', {})
 
         return newid
 
@@ -913,6 +922,10 @@ class Class(hyperdb.Class):
             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):
@@ -954,6 +967,10 @@ class Class(hyperdb.Class):
                 value = pwd
             d[propname] = value
 
+        # check retired flag
+        if int(proplist[-1]):
+            d[self.db.RETIRED_FLAG] = 1
+
         # add the node and journal
         self.db.addnode(self.classname, newid, d)
 
@@ -970,7 +987,7 @@ class Class(hyperdb.Class):
             creation = None
         if d.has_key('activity'):
             del d['activity']
-        self.db.addjournal(self.classname, newid, 'create', d, creator,
+        self.db.addjournal(self.classname, newid, 'create', {}, creator,
             creation)
         return newid
 
@@ -1145,9 +1162,11 @@ class Class(hyperdb.Class):
 
             # if the value's the same as the existing value, no sense in
             # doing anything
-            if node.has_key(propname) and value == node[propname]:
+            current = node.get(propname, None)
+            if value == current:
                 del propvalues[propname]
                 continue
+            journalvalues[propname] = current
 
             # do stuff based on the prop type
             if isinstance(prop, Link):
@@ -1242,7 +1261,7 @@ class Class(hyperdb.Class):
                     journalvalues[propname] = tuple(l)
 
             elif isinstance(prop, String):
-                if value is not None and type(value) != type(''):
+                if value is not None and type(value) != type('') and type(value) != type(u''):
                     raise TypeError, 'new property "%s" not a string'%propname
 
             elif isinstance(prop, Password):
@@ -1283,8 +1302,7 @@ class Class(hyperdb.Class):
         self.db.setnode(self.classname, nodeid, node)
 
         if self.do_journal:
-            propvalues.update(journalvalues)
-            self.db.addjournal(self.classname, nodeid, 'set', propvalues)
+            self.db.addjournal(self.classname, nodeid, 'set', journalvalues)
 
         self.fireReactors('set', nodeid, oldvalues)
 
@@ -1315,17 +1333,17 @@ class Class(hyperdb.Class):
 
         self.fireReactors('retire', nodeid, None)
 
-    def is_retired(self, nodeid):
+    def is_retired(self, nodeid, cldb=None):
         '''Return true if the node is retired.
         '''
-        node = self.db.getnode(cn, nodeid, cldb)
+        node = self.db.getnode(self.classname, nodeid, cldb)
         if node.has_key(self.db.RETIRED_FLAG):
             return 1
         return 0
 
     def destroy(self, nodeid):
         '''Destroy a node.
-        
+
         WARNING: this method should never be used except in extremely rare
                  situations where there could never be links to the node being
                  deleted
@@ -1348,7 +1366,7 @@ class Class(hyperdb.Class):
 
         The returned list contains tuples of the form
 
-            (date, tag, action, params)
+            (nodeid, date, tag, action, params)
 
         'date' is a Timestamp object specifying the time of the change and
         'tag' is the journaltag specified when the database was opened.
@@ -1422,7 +1440,6 @@ class Class(hyperdb.Class):
                 if node.has_key(self.db.RETIRED_FLAG):
                     continue
                 if node[self.key] == keyvalue:
-                    cldb.close()
                     return nodeid
         finally:
             cldb.close()
@@ -1603,6 +1620,10 @@ class Class(hyperdb.Class):
                 else:
                     bv = v
                 l.append((OTHER, k, bv))
+            elif isinstance(propclass, Date):
+                l.append((OTHER, k, date.Date(v)))
+            elif isinstance(propclass, Interval):
+                l.append((OTHER, k, date.Interval(v)))
             elif isinstance(propclass, Number):
                 l.append((OTHER, k, int(v)))
             else:
@@ -1612,7 +1633,6 @@ class Class(hyperdb.Class):
         # now, find all the nodes that are active and pass filtering
         l = []
         cldb = self.db.getclassdb(cn)
-        print filterspec
         try:
             # TODO: only full-scan once (use items())
             for nodeid in self.db.getnodeids(cn, cldb):
@@ -1704,9 +1724,9 @@ class Class(hyperdb.Class):
                 if isinstance(propclass, String):
                     # clean up the strings
                     if av and av[0] in string.uppercase:
-                        av = an[prop] = av.lower()
+                        av = av.lower()
                     if bv and bv[0] in string.uppercase:
-                        bv = bn[prop] = bv.lower()
+                        bv = bv.lower()
                 if (isinstance(propclass, String) or
                         isinstance(propclass, Date)):
                     # it might be a string that's really an integer
@@ -1759,12 +1779,15 @@ class Class(hyperdb.Class):
                 # Multilink properties are sorted according to how many
                 # links are present.
                 elif isinstance(propclass, Multilink):
+                    r = cmp(len(av), len(bv))
+                    if r == 0:
+                        # Compare contents of multilink property if lenghts is
+                        # equal
+                        r = cmp ('.'.join(av), '.'.join(bv))
                     if dir == '+':
-                        r = cmp(len(av), len(bv))
-                        if r != 0: return r
+                        return r
                     elif dir == '-':
-                        r = cmp(len(bv), len(av))
-                        if r != 0: return r
+                        return -r
                 elif isinstance(propclass, Number) or isinstance(propclass, Boolean):
                     if dir == '+':
                         r = cmp(av, bv)
@@ -1865,7 +1888,7 @@ class Class(hyperdb.Class):
         for react in self.reactors[action]:
             react(self.db, self, nodeid, oldvalues)
 
-class FileClass(Class):
+class FileClass(Class, hyperdb.FileClass):
     '''This class defines a large chunk of data. To support this, it has a
        mandatory String property "content" which is typically saved off
        externally to the hyperdb.
@@ -1877,11 +1900,23 @@ class FileClass(Class):
     default_mime_type = 'text/plain'
 
     def create(self, **propvalues):
-        ''' snaffle the file propvalue and store in a file
+        ''' Snarf the "content" propvalue and store in a file
         '''
+        # we need to fire the auditors now, or the content property won't
+        # be in propvalues for the auditors to play with
+        self.fireAuditors('create', None, propvalues)
+
+        # now remove the content property so it's not stored in the db
         content = propvalues['content']
         del propvalues['content']
-        newid = Class.create(self, **propvalues)
+
+        # do the database create
+        newid = Class.create_inner(self, **propvalues)
+
+        # fire reactors
+        self.fireReactors('create', newid, None)
+
+        # store off the content as a file
         self.db.storefile(self.classname, newid, None, content)
         return newid
 
@@ -1907,13 +1942,12 @@ class FileClass(Class):
     def get(self, nodeid, propname, default=_marker, cache=1):
         ''' trap the content propname and get it from the file
         '''
-
-        poss_msg = 'Possibly a access right configuration problem.'
+        poss_msg = 'Possibly an access right configuration problem.'
         if propname == 'content':
             try:
                 return self.db.getfile(self.classname, nodeid, None)
             except IOError, (strerror):
-                # BUG: by catching this we donot see an error in the log.
+                # 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)
         if default is not _marker: