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):

Roundup Issue Tracker: http://roundup-tracker.org/