Mercurial > p > roundup > code
comparison roundup/hyperdb.py @ 8304:24549122f9b1
Factor common code to hyperdb/roundupdb
There was some common copied code in backends/back_anydbm.py and
backends/rdbms_common.py. This is now moved to hyperdb.py and
roundupdb.py, respectively (the FileClass lives in hyperdb.py while the
IssueClass is in roundupdb.py)
| author | Ralf Schlatterbeck <rsc@runtux.com> |
|---|---|
| date | Sat, 01 Mar 2025 13:08:09 +0100 |
| parents | b99e76e76496 |
| children | a81a3cd067fa |
comparison
equal
deleted
inserted
replaced
| 8303:45ec660eb7f7 | 8304:24549122f9b1 |
|---|---|
| 19 """Hyperdatabase implementation, especially field types. | 19 """Hyperdatabase implementation, especially field types. |
| 20 """ | 20 """ |
| 21 __docformat__ = 'restructuredtext' | 21 __docformat__ = 'restructuredtext' |
| 22 | 22 |
| 23 # standard python modules | 23 # standard python modules |
| 24 import copy | |
| 24 import logging | 25 import logging |
| 25 import os | 26 import os |
| 26 import re | 27 import re |
| 27 import shutil | 28 import shutil |
| 28 import sys | 29 import sys |
| 29 import traceback | 30 import traceback |
| 30 import weakref | 31 import weakref |
| 31 | 32 |
| 33 from hashlib import md5 | |
| 34 | |
| 32 # roundup modules | 35 # roundup modules |
| 33 from . import date, password | 36 from . import date, password |
| 34 from .support import ensureParentsExist, PrioList | 37 from .support import ensureParentsExist, PrioList |
| 35 from roundup.mlink_expr import Expression | 38 from roundup.mlink_expr import Expression |
| 36 from roundup.i18n import _ | 39 from roundup.i18n import _ |
| 37 from roundup.cgi.exceptions import DetectorError | 40 from roundup.cgi.exceptions import DetectorError |
| 38 from roundup.anypy.cmp_ import NoneAndDictComparable | 41 from roundup.anypy.cmp_ import NoneAndDictComparable |
| 39 from roundup.anypy.strings import eval_import | 42 from roundup.anypy.strings import b2s, bs2b, eval_import |
| 40 | 43 |
| 41 logger = logging.getLogger('roundup.hyperdb') | 44 logger = logging.getLogger('roundup.hyperdb') |
| 45 | |
| 46 # marker used for an unspecified keyword argument | |
| 47 _marker = [] | |
| 42 | 48 |
| 43 | 49 |
| 44 # | 50 # |
| 45 # Types | 51 # Types |
| 46 # | 52 # |
| 1226 | 1232 |
| 1227 If an id in a link or multilink property does not refer to a valid | 1233 If an id in a link or multilink property does not refer to a valid |
| 1228 node, an IndexError is raised. | 1234 node, an IndexError is raised. |
| 1229 """ | 1235 """ |
| 1230 raise NotImplementedError | 1236 raise NotImplementedError |
| 1231 | |
| 1232 _marker = [] | |
| 1233 | 1237 |
| 1234 def get(self, nodeid, propname, default=_marker, cache=1): | 1238 def get(self, nodeid, propname, default=_marker, cache=1): |
| 1235 """Get the value of a property on an existing node of this class. | 1239 """Get the value of a property on an existing node of this class. |
| 1236 | 1240 |
| 1237 'nodeid' must be the id of an existing node of this class or an | 1241 'nodeid' must be the id of an existing node of this class or an |
| 2096 | 2100 |
| 2097 return value | 2101 return value |
| 2098 | 2102 |
| 2099 | 2103 |
| 2100 class FileClass: | 2104 class FileClass: |
| 2101 """ A class that requires the "content" property and stores it on | 2105 """ This class defines a large chunk of data. To support this, it |
| 2102 disk. | 2106 has a mandatory String property "content" which is saved off |
| 2107 externally to the hyperdb. | |
| 2108 | |
| 2109 The default MIME type of this data is defined by the | |
| 2110 "default_mime_type" class attribute, which may be overridden by | |
| 2111 each node if the class defines a "type" String property. | |
| 2103 """ | 2112 """ |
| 2104 default_mime_type = 'text/plain' | 2113 default_mime_type = 'text/plain' |
| 2105 | 2114 |
| 2106 def __init__(self, db, classname, **properties): | 2115 def _update_properties(self, properties): |
| 2107 """The newly-created class automatically includes the "content" | 2116 """The newly-created class automatically includes the "content" |
| 2108 property. | 2117 and "type" properties. This method must be called by __init__. |
| 2109 """ | 2118 """ |
| 2110 if 'content' not in properties: | 2119 if 'content' not in properties: |
| 2111 properties['content'] = String(indexme='yes') | 2120 properties['content'] = String(indexme='yes') |
| 2121 if 'type' not in properties: | |
| 2122 properties['type'] = String() | |
| 2123 | |
| 2124 def create(self, **propvalues): | |
| 2125 """ snaffle the file propvalue and store in a file | |
| 2126 """ | |
| 2127 # we need to fire the auditors now, or the content property won't | |
| 2128 # be in propvalues for the auditors to play with | |
| 2129 self.fireAuditors('create', None, propvalues) | |
| 2130 | |
| 2131 # now remove the content property so it's not stored in the db | |
| 2132 content = propvalues['content'] | |
| 2133 del propvalues['content'] | |
| 2134 | |
| 2135 # do the database create | |
| 2136 newid = self.create_inner(**propvalues) | |
| 2137 | |
| 2138 # figure the mime type | |
| 2139 mime_type = propvalues.get('type', self.default_mime_type) | |
| 2140 | |
| 2141 # optionally index | |
| 2142 # This wasn't done for the anydbm backend (but the 'set' method | |
| 2143 # *did* update the index) so this is probably a bug-fix for anydbm | |
| 2144 if self.properties['content'].indexme: | |
| 2145 index_content = content | |
| 2146 if bytes != str and isinstance(content, bytes): | |
| 2147 index_content = content.decode('utf-8', errors='ignore') | |
| 2148 self.db.indexer.add_text((self.classname, newid, 'content'), | |
| 2149 index_content, mime_type) | |
| 2150 | |
| 2151 # store off the content as a file | |
| 2152 self.db.storefile(self.classname, newid, None, bs2b(content)) | |
| 2153 | |
| 2154 # fire reactors | |
| 2155 self.fireReactors('create', newid, None) | |
| 2156 | |
| 2157 return newid | |
| 2112 | 2158 |
| 2113 def export_propnames(self): | 2159 def export_propnames(self): |
| 2114 """ Don't export the "content" property | 2160 """ Don't export the "content" property |
| 2115 """ | 2161 """ |
| 2116 propnames = list(self.getprops().keys()) | 2162 propnames = list(self.getprops().keys()) |
| 2133 source = self.db.filename(self.classname, nodeid) | 2179 source = self.db.filename(self.classname, nodeid) |
| 2134 | 2180 |
| 2135 dest = self.exportFilename(dirname, nodeid) | 2181 dest = self.exportFilename(dirname, nodeid) |
| 2136 ensureParentsExist(dest) | 2182 ensureParentsExist(dest) |
| 2137 shutil.copyfile(source, dest) | 2183 shutil.copyfile(source, dest) |
| 2184 | |
| 2185 def get(self, nodeid, propname, default=_marker, cache=1): | |
| 2186 """ Trap the content propname and get it from the file | |
| 2187 | |
| 2188 'cache' exists for backwards compatibility, and is not used. | |
| 2189 """ | |
| 2190 poss_msg = 'Possibly an access right configuration problem.' | |
| 2191 if propname == 'content': | |
| 2192 try: | |
| 2193 return b2s(self.db.getfile(self.classname, nodeid, None)) | |
| 2194 except IOError as strerror: | |
| 2195 # BUG: by catching this we don't see an error in the log. | |
| 2196 return 'ERROR reading file: %s%s\n%s\n%s' % ( | |
| 2197 self.classname, nodeid, poss_msg, strerror) | |
| 2198 except UnicodeDecodeError: | |
| 2199 # if content is not text (e.g. jpeg file) we get | |
| 2200 # unicode error trying to convert to string in python 3. | |
| 2201 # trap it and supply an error message. Include md5sum | |
| 2202 # of content as this string is included in the etag | |
| 2203 # calculation of the object. | |
| 2204 return ('%s%s is not text, retrieve using ' | |
| 2205 'binary_content property. mdsum: %s') % ( | |
| 2206 self.classname, nodeid, | |
| 2207 md5(self.db.getfile( | |
| 2208 self.classname, | |
| 2209 nodeid, | |
| 2210 None)).hexdigest()) # nosec - bandit md5 use ok | |
| 2211 elif propname == 'binary_content': | |
| 2212 return self.db.getfile(self.classname, nodeid, None) | |
| 2213 | |
| 2214 if default is not _marker: | |
| 2215 return self.subclass.get(self, nodeid, propname, default) | |
| 2216 else: | |
| 2217 return self.subclass.get(self, nodeid, propname) | |
| 2138 | 2218 |
| 2139 def import_files(self, dirname, nodeid): | 2219 def import_files(self, dirname, nodeid): |
| 2140 """ Import the "content" property as a file | 2220 """ Import the "content" property as a file |
| 2141 """ | 2221 """ |
| 2142 source = self.exportFilename(dirname, nodeid) | 2222 source = self.exportFilename(dirname, nodeid) |
| 2159 # other types. So if mime type of file is correct, we | 2239 # other types. So if mime type of file is correct, we |
| 2160 # call add_text on content. | 2240 # call add_text on content. |
| 2161 self.db.indexer.add_text((self.classname, nodeid, 'content'), | 2241 self.db.indexer.add_text((self.classname, nodeid, 'content'), |
| 2162 index_content, mime_type) | 2242 index_content, mime_type) |
| 2163 | 2243 |
| 2244 def index(self, nodeid): | |
| 2245 """ Add (or refresh) the node to search indexes. | |
| 2246 | |
| 2247 Use the content-type property for the content property. | |
| 2248 """ | |
| 2249 # find all the String properties that have indexme | |
| 2250 for prop, propclass in self.getprops().items(): | |
| 2251 if prop == 'content' and propclass.indexme: | |
| 2252 mime_type = self.get(nodeid, 'type', self.default_mime_type) | |
| 2253 index_content = self.get(nodeid, 'binary_content') | |
| 2254 if bytes != str and isinstance(index_content, bytes): | |
| 2255 index_content = index_content.decode('utf-8', | |
| 2256 errors='ignore') | |
| 2257 self.db.indexer.add_text((self.classname, nodeid, 'content'), | |
| 2258 index_content, mime_type) | |
| 2259 elif isinstance(propclass, String) and propclass.indexme: | |
| 2260 # index them under (classname, nodeid, property) | |
| 2261 try: | |
| 2262 value = str(self.get(nodeid, prop)) | |
| 2263 except IndexError: | |
| 2264 # node has been destroyed | |
| 2265 continue | |
| 2266 self.db.indexer.add_text((self.classname, nodeid, prop), value) | |
| 2267 | |
| 2268 def set(self, itemid, **propvalues): | |
| 2269 """ Snarf the "content" propvalue and update it in a file | |
| 2270 """ | |
| 2271 self.fireAuditors('set', itemid, propvalues) | |
| 2272 | |
| 2273 # create the oldvalues dict - fill in any missing values | |
| 2274 oldvalues = copy.deepcopy(self.db.getnode(self.classname, itemid)) | |
| 2275 # The following is redundant for rdbms backends but needed for anydbm | |
| 2276 # the performance impact is so low we that we don't factor this. | |
| 2277 for name, prop in self.getprops(protected=0).items(): | |
| 2278 if name in oldvalues: | |
| 2279 continue | |
| 2280 if isinstance(prop, Multilink): | |
| 2281 oldvalues[name] = [] | |
| 2282 else: | |
| 2283 oldvalues[name] = None | |
| 2284 | |
| 2285 # now remove the content property so it's not stored in the db | |
| 2286 content = None | |
| 2287 if 'content' in propvalues: | |
| 2288 content = propvalues['content'] | |
| 2289 del propvalues['content'] | |
| 2290 | |
| 2291 # do the database update | |
| 2292 propvalues = self.set_inner(itemid, **propvalues) | |
| 2293 | |
| 2294 # do content? | |
| 2295 if content: | |
| 2296 # store and possibly index | |
| 2297 self.db.storefile(self.classname, itemid, None, bs2b(content)) | |
| 2298 if self.properties['content'].indexme: | |
| 2299 index_content = content | |
| 2300 if bytes != str and isinstance(content, bytes): | |
| 2301 index_content = content.decode('utf-8', errors='ignore') | |
| 2302 mime_type = self.get(itemid, 'type', self.default_mime_type) | |
| 2303 self.db.indexer.add_text((self.classname, itemid, 'content'), | |
| 2304 index_content, mime_type) | |
| 2305 propvalues['content'] = content | |
| 2306 | |
| 2307 # fire reactors | |
| 2308 self.fireReactors('set', itemid, oldvalues) | |
| 2309 return propvalues | |
| 2164 | 2310 |
| 2165 class Node: | 2311 class Node: |
| 2166 """ A convenience wrapper for the given node | 2312 """ A convenience wrapper for the given node |
| 2167 """ | 2313 """ |
| 2168 def __init__(self, cl, nodeid, cache=1): | 2314 def __init__(self, cl, nodeid, cache=1): |
