Code

. #565996 ] The "Attach a File to this Issue" fails
[roundup.git] / roundup / backends / back_anydbm.py
index b7e16155d2dd9b5f3622efded54a64e213147dd5..5ea64da8956f9f76bf1daccdd1f7cc0bbaf3c4cf 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.26 2002-01-22 05:18:38 rochecompaan Exp $
+#$Id: back_anydbm.py,v 1.35 2002-05-25 07:16:24 rochecompaan 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
@@ -24,14 +24,15 @@ serious bugs, and is not available)
 '''
 
 import whichdb, anydbm, os, marshal
-from roundup import hyperdb, date, password
-
-DEBUG=os.environ.get('HYPERDBDEBUG', '')
+from roundup import hyperdb, date
+from blobfiles import FileStorage
+from roundup.roundup_indexer import RoundupIndexer
+from locking import acquire_lock, release_lock
 
 #
 # Now the database
 #
-class Database(hyperdb.Database):
+class Database(FileStorage, hyperdb.Database):
     """A database for storing records containing flexible data types.
 
     Transaction stuff TODO:
@@ -61,6 +62,9 @@ class Database(hyperdb.Database):
         self.dirtynodes = {}    # keep track of the dirty nodes by class
         self.newnodes = {}      # keep track of the new nodes by class
         self.transactions = []
+        self.indexer = RoundupIndexer(self.dir)
+        # ensure files are group readable and writable
+        os.umask(0002)
 
     def __repr__(self):
         return '<back_anydbm instance at %x>'%id(self) 
@@ -71,14 +75,14 @@ class Database(hyperdb.Database):
     def __getattr__(self, classname):
         """A convenient way of calling self.getclass(classname)."""
         if self.classes.has_key(classname):
-            if DEBUG:
-                print '__getattr__', (self, classname)
+            if __debug__:
+                print >>hyperdb.DEBUG, '__getattr__', (self, classname)
             return self.classes[classname]
         raise AttributeError, classname
 
     def addclass(self, cl):
-        if DEBUG:
-            print 'addclass', (self, cl)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'addclass', (self, cl)
         cn = cl.classname
         if self.classes.has_key(cn):
             raise ValueError, cn
@@ -86,8 +90,8 @@ class Database(hyperdb.Database):
 
     def getclasses(self):
         """Return a list of the names of all existing classes."""
-        if DEBUG:
-            print 'getclasses', (self,)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'getclasses', (self,)
         l = self.classes.keys()
         l.sort()
         return l
@@ -97,8 +101,8 @@ class Database(hyperdb.Database):
 
         If 'classname' is not a valid class name, a KeyError is raised.
         """
-        if DEBUG:
-            print 'getclass', (self, classname)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'getclass', (self, classname)
         return self.classes[classname]
 
     #
@@ -107,10 +111,10 @@ class Database(hyperdb.Database):
     def clear(self):
         '''Delete all database contents
         '''
-        if DEBUG:
-            print 'clear', (self,)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'clear', (self,)
         for cn in self.classes.keys():
-            for type in 'nodes', 'journals':
+            for dummy in 'nodes', 'journals':
                 path = os.path.join(self.dir, 'journals.%s'%cn)
                 if os.path.exists(path):
                     os.remove(path)
@@ -121,16 +125,17 @@ class Database(hyperdb.Database):
         ''' grab a connection to the class db that will be used for
             multiple actions
         '''
-        if DEBUG:
-            print 'getclassdb', (self, classname, mode)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'getclassdb', (self, classname, mode)
         return self._opendb('nodes.%s'%classname, mode)
 
     def _opendb(self, name, mode):
         '''Low-level database opener that gets around anydbm/dbm
            eccentricities.
         '''
-        if DEBUG:
-            print '_opendb', (self, name, mode)
+        if __debug__:
+            print >>hyperdb.DEBUG, '_opendb', (self, name, mode)
+
         # determine which DB wrote the class file
         db_type = ''
         path = os.path.join(os.getcwd(), self.dir, name)
@@ -145,8 +150,8 @@ class Database(hyperdb.Database):
 
         # new database? let anydbm pick the best dbm
         if not db_type:
-            if DEBUG:
-                print "_opendb anydbm.open(%r, 'n')"%path
+            if __debug__:
+                print >>hyperdb.DEBUG, "_opendb anydbm.open(%r, 'n')"%path
             return anydbm.open(path, 'n')
 
         # open the database with the correct module
@@ -156,18 +161,44 @@ class Database(hyperdb.Database):
             raise hyperdb.DatabaseError, \
                 "Couldn't open database - the required module '%s'"\
                 "is not available"%db_type
-        if DEBUG:
-            print "_opendb %r.open(%r, %r)"%(db_type, path, mode)
+        if __debug__:
+            print >>hyperdb.DEBUG, "_opendb %r.open(%r, %r)"%(db_type, path,
+                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
+    #
+    def newid(self, classname):
+        ''' 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)
+        else:
+            # the count() bit is transitional - older dbs won't start at 1
+            newid = str(self.getclass(classname).count()+1)
+            db[classname] = newid
+        db.close()
+        release_lock(lock)
+        return newid
+
     #
     # Nodes
     #
     def addnode(self, classname, nodeid, node):
         ''' add the specified node to its class's db
         '''
-        if DEBUG:
-            print 'addnode', (self, classname, nodeid, node)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
         self.newnodes.setdefault(classname, {})[nodeid] = 1
         self.cache.setdefault(classname, {})[nodeid] = node
         self.savenode(classname, nodeid, node)
@@ -175,9 +206,10 @@ class Database(hyperdb.Database):
     def setnode(self, classname, nodeid, node):
         ''' change the specified node
         '''
-        if DEBUG:
-            print 'setnode', (self, classname, nodeid, node)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, node)
         self.dirtynodes.setdefault(classname, {})[nodeid] = 1
+
         # can't set without having already loaded the node
         self.cache[classname][nodeid] = node
         self.savenode(classname, nodeid, node)
@@ -185,40 +217,59 @@ class Database(hyperdb.Database):
     def savenode(self, classname, nodeid, node):
         ''' perform the saving of data specified by the set/addnode
         '''
-        if DEBUG:
-            print 'savenode', (self, classname, nodeid, node)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'savenode', (self, classname, nodeid, node)
         self.transactions.append((self._doSaveNode, (classname, nodeid, node)))
 
     def getnode(self, classname, nodeid, db=None, cache=1):
         ''' get a node from the database
         '''
-        if DEBUG:
-            print 'getnode', (self, classname, nodeid, cldb)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid, db)
         if cache:
             # try the cache
-            cache = self.cache.setdefault(classname, {})
-            if cache.has_key(nodeid):
-                return cache[nodeid]
+            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]
+
+        if __debug__:
+            print >>hyperdb.TRACE, 'get %s %s'%(classname, nodeid)
 
         # get from the database and save in the cache
         if db is None:
             db = self.getclassdb(classname)
         if not db.has_key(nodeid):
             raise IndexError, "no such %s %s"%(classname, nodeid)
+
+        # decode
         res = marshal.loads(db[nodeid])
+
+        # reverse the serialisation
+        res = self.unserialise(classname, res)
+
+        # store off in the cache dict
         if cache:
-            cache[nodeid] = res
+            cache_dict[nodeid] = res
+
         return res
 
     def hasnode(self, classname, nodeid, db=None):
         ''' determine if the database has a given node
         '''
-        if DEBUG:
-            print 'hasnode', (self, classname, nodeid, cldb)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'hasnode', (self, classname, nodeid, db)
+
         # try the cache
         cache = self.cache.setdefault(classname, {})
         if cache.has_key(nodeid):
+            if __debug__:
+                print >>hyperdb.TRACE, 'has %s %s cached'%(classname, nodeid)
             return 1
+        if __debug__:
+            print >>hyperdb.TRACE, 'has %s %s'%(classname, nodeid)
 
         # not in the cache - check the database
         if db is None:
@@ -227,8 +278,8 @@ class Database(hyperdb.Database):
         return res
 
     def countnodes(self, classname, db=None):
-        if DEBUG:
-            print 'countnodes', (self, classname, cldb)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'countnodes', (self, classname, db)
         # include the new nodes not saved to the DB yet
         count = len(self.newnodes.get(classname, {}))
 
@@ -239,8 +290,8 @@ class Database(hyperdb.Database):
         return count
 
     def getnodeids(self, classname, db=None):
-        if DEBUG:
-            print 'getnodeids', (self, classname, db)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'getnodeids', (self, classname, db)
         # start off with the new nodes
         res = self.newnodes.get(classname, {}).keys()
 
@@ -252,36 +303,7 @@ class Database(hyperdb.Database):
 
     #
     # Files - special node properties
-    #
-    def filename(self, classname, nodeid, property=None):
-        '''Determine what the filename for the given node and optionally property is.
-        '''
-        # TODO: split into multiple files directories
-        if property:
-            return os.path.join(self.dir, 'files', '%s%s.%s'%(classname,
-                nodeid, property))
-        else:
-            # roundupdb.FileClass never specified the property name, so don't include it
-            return os.path.join(self.dir, 'files', '%s%s'%(classname,
-                nodeid))
-
-    def storefile(self, classname, nodeid, property, content):
-        '''Store the content of the file in the database. The property may be None, in
-           which case the filename does not indicate which property is being saved.
-        '''
-        name = self.filename(classname, nodeid, property)
-        open(name + '.tmp', 'wb').write(content)
-        self.transactions.append((self._doStoreFile, (name, )))
-
-    def getfile(self, classname, nodeid, property):
-        '''Store the content of the file in the database.
-        '''
-        filename = self.filename(classname, nodeid, property)
-        try:
-            return open(filename, 'rb').read()
-        except:
-            return open(filename+'.tmp', 'rb').read()
-
+    # inherited from FileStorage
 
     #
     # Journal
@@ -294,16 +316,17 @@ class Database(hyperdb.Database):
             'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
             'retire' -- 'params' is None
         '''
-        if DEBUG:
-            print 'addjournal', (self, classname, nodeid, action, params)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'addjournal', (self, classname, nodeid,
+                action, params)
         self.transactions.append((self._doSaveJournal, (classname, nodeid,
             action, params)))
 
     def getjournal(self, classname, nodeid):
         ''' get the journal for id
         '''
-        if DEBUG:
-            print 'getjournal', (self, classname, nodeid)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'getjournal', (self, classname, nodeid)
         # attempt to open the journal - in some rare cases, the journal may
         # not exist
         try:
@@ -315,15 +338,15 @@ class Database(hyperdb.Database):
         journal = marshal.loads(db[nodeid])
         res = []
         for entry in journal:
-            (nodeid, date_stamp, self.journaltag, action, params) = entry
+            (nodeid, date_stamp, user, action, params) = entry
             date_obj = date.Date(date_stamp)
-            res.append((nodeid, date_obj, self.journaltag, action, params))
+            res.append((nodeid, date_obj, user, action, params))
         return res
 
     def pack(self, pack_before):
         ''' delete all journal entries before 'pack_before' '''
-        if DEBUG:
-            print 'packjournal', (self, pack_before)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'packjournal', (self, pack_before)
 
         pack_before = pack_before.get_tuple()
 
@@ -375,8 +398,8 @@ class Database(hyperdb.Database):
     def commit(self):
         ''' Commit the current transactions.
         '''
-        if DEBUG:
-            print 'commit', (self,)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'commit', (self,)
         # TODO: lock the DB
 
         # keep a handle to all the database files opened
@@ -399,8 +422,9 @@ class Database(hyperdb.Database):
         self.transactions = []
 
     def _doSaveNode(self, classname, nodeid, node):
-        if DEBUG:
-            print '_doSaveNode', (self, classname, nodeid, node)
+        if __debug__:
+            print >>hyperdb.DEBUG, '_doSaveNode', (self, classname, nodeid,
+                node)
 
         # get the database handle
         db_name = 'nodes.%s'%classname
@@ -410,14 +434,20 @@ class Database(hyperdb.Database):
             db = self.databases[db_name] = self.getclassdb(classname, 'c')
 
         # now save the marshalled data
-        db[nodeid] = marshal.dumps(node)
+        db[nodeid] = marshal.dumps(self.serialise(classname, node))
 
     def _doSaveJournal(self, classname, nodeid, action, params):
-        if DEBUG:
-            print '_doSaveJournal', (self, classname, nodeid, action, params)
+        # serialise first
+        if action in ('set', 'create'):
+            params = self.serialise(classname, params)
+
+        # create the journal entry
         entry = (nodeid, date.Date().get_tuple(), self.journaltag, action,
             params)
 
+        if __debug__:
+            print >>hyperdb.DEBUG, '_doSaveJournal', entry
+
         # get the database handle
         db_name = 'journals.%s'%classname
         if self.databases.has_key(db_name):
@@ -427,22 +457,27 @@ class Database(hyperdb.Database):
 
         # now insert the journal entry
         if db.has_key(nodeid):
+            # append to existing
             s = db[nodeid]
-            l = marshal.loads(db[nodeid])
+            l = marshal.loads(s)
             l.append(entry)
         else:
             l = [entry]
+
         db[nodeid] = marshal.dumps(l)
 
     def _doStoreFile(self, name, **databases):
         # the file is currently ".tmp" - move it to its real name to commit
         os.rename(name+".tmp", name)
+        pattern = name.split('/')[-1]
+        self.indexer.add_files(dir=os.path.dirname(name), pattern=pattern)
+        self.indexer.save_index()
 
     def rollback(self):
         ''' Reverse all actions from the current transaction.
         '''
-        if DEBUG:
-            print 'rollback', (self, )
+        if __debug__:
+            print >>hyperdb.DEBUG, 'rollback', (self, )
         for method, args in self.transactions:
             # delete temporary files
             if method == self._doStoreFile:
@@ -455,6 +490,56 @@ class Database(hyperdb.Database):
 
 #
 #$Log: not supported by cvs2svn $
+#Revision 1.34  2002/05/15 06:21:21  richard
+# . node caching now works, and gives a small boost in performance
+#
+#As a part of this, I cleaned up the DEBUG output and implemented TRACE
+#output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
+#CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
+#(using if __debug__ which is compiled out with -O)
+#
+#Revision 1.33  2002/04/24 10:38:26  rochecompaan
+#All database files are now created group readable and writable.
+#
+#Revision 1.32  2002/04/15 23:25:15  richard
+#. node ids are now generated from a lockable store - no more race conditions
+#
+#We're using the portalocker code by Jonathan Feinberg that was contributed
+#to the ASPN Python cookbook. This gives us locking across Unix and Windows.
+#
+#Revision 1.31  2002/04/03 05:54:31  richard
+#Fixed serialisation problem by moving the serialisation step out of the
+#hyperdb.Class (get, set) into the hyperdb.Database.
+#
+#Also fixed htmltemplate after the showid changes I made yesterday.
+#
+#Unit tests for all of the above written.
+#
+#Revision 1.30.2.1  2002/04/03 11:55:57  rochecompaan
+# . Added feature #526730 - search for messages capability
+#
+#Revision 1.30  2002/02/27 03:40:59  richard
+#Ran it through pychecker, made fixes
+#
+#Revision 1.29  2002/02/25 14:34:31  grubert
+# . use blobfiles in back_anydbm which is used in back_bsddb.
+#   change test_db as dirlist does not work for subdirectories.
+#   ATTENTION: blobfiles now creates subdirectories for files.
+#
+#Revision 1.28  2002/02/16 09:14:17  richard
+# . #514854 ] History: "User" is always ticket creator
+#
+#Revision 1.27  2002/01/22 07:21:13  richard
+#. fixed back_bsddb so it passed the journal tests
+#
+#... it didn't seem happy using the back_anydbm _open method, which is odd.
+#Yet another occurrance of whichdb not being able to recognise older bsddb
+#databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
+#process.
+#
+#Revision 1.26  2002/01/22 05:18:38  rochecompaan
+#last_set_entry was referenced before assignment
+#
 #Revision 1.25  2002/01/22 05:06:08  rochecompaan
 #We need to keep the last 'set' entry in the journal to preserve
 #information on 'activity' for nodes.