dumbdbm.py revision ba426640ddeba29877ce9ebd750864aef7faeef9
1"""A dumb and slow but simple dbm clone. 2 3For database spam, spam.dir contains the index (a text file), 4spam.bak *may* contain a backup of the index (also a text file), 5while spam.dat contains the data (a binary file). 6 7XXX TO DO: 8 9- seems to contain a bug when updating... 10 11- reclaim free space (currently, space once occupied by deleted or expanded 12items is never reused) 13 14- support concurrent access (currently, if two processes take turns making 15updates, they can mess up the index) 16 17- support efficient access to large databases (currently, the whole index 18is read when the database is opened, and some updates rewrite the whole index) 19 20- support opening for read-only (flag = 'm') 21 22""" 23 24_os = __import__('os') 25import __builtin__ 26 27_open = __builtin__.open 28 29_BLOCKSIZE = 512 30 31class _Database: 32 33 def __init__(self, file): 34 self._dirfile = file + '.dir' 35 self._datfile = file + '.dat' 36 self._bakfile = file + '.bak' 37 # Mod by Jack: create data file if needed 38 try: 39 f = _open(self._datfile, 'r') 40 except IOError: 41 f = _open(self._datfile, 'w') 42 f.close() 43 self._update() 44 45 def _update(self): 46 self._index = {} 47 try: 48 f = _open(self._dirfile) 49 except IOError: 50 pass 51 else: 52 while 1: 53 line = f.readline() 54 if not line: break 55 key, (pos, siz) = eval(line) 56 self._index[key] = (pos, siz) 57 f.close() 58 59 def _commit(self): 60 try: _os.unlink(self._bakfile) 61 except _os.error: pass 62 try: _os.rename(self._dirfile, self._bakfile) 63 except _os.error: pass 64 f = _open(self._dirfile, 'w') 65 for key, (pos, siz) in self._index.items(): 66 f.write("%s, (%s, %s)\n" % (`key`, `pos`, `siz`)) 67 f.close() 68 69 def __getitem__(self, key): 70 pos, siz = self._index[key] # may raise KeyError 71 f = _open(self._datfile, 'rb') 72 f.seek(pos) 73 dat = f.read(siz) 74 f.close() 75 return dat 76 77 def _addval(self, val): 78 f = _open(self._datfile, 'rb+') 79 f.seek(0, 2) 80 pos = f.tell() 81## Does not work under MW compiler 82## pos = ((pos + _BLOCKSIZE - 1) / _BLOCKSIZE) * _BLOCKSIZE 83## f.seek(pos) 84 npos = ((pos + _BLOCKSIZE - 1) / _BLOCKSIZE) * _BLOCKSIZE 85 f.write('\0'*(npos-pos)) 86 pos = npos 87 88 f.write(val) 89 f.close() 90 return (pos, len(val)) 91 92 def _setval(self, pos, val): 93 f = _open(self._datfile, 'rb+') 94 f.seek(pos) 95 f.write(val) 96 f.close() 97 return (pos, len(val)) 98 99 def _addkey(self, key, (pos, siz)): 100 self._index[key] = (pos, siz) 101 f = _open(self._dirfile, 'a') 102 f.write("%s, (%s, %s)\n" % (`key`, `pos`, `siz`)) 103 f.close() 104 105 def __setitem__(self, key, val): 106 if not type(key) == type('') == type(val): 107 raise TypeError, "keys and values must be strings" 108 if not self._index.has_key(key): 109 (pos, siz) = self._addval(val) 110 self._addkey(key, (pos, siz)) 111 else: 112 pos, siz = self._index[key] 113 oldblocks = (siz + _BLOCKSIZE - 1) / _BLOCKSIZE 114 newblocks = (len(val) + _BLOCKSIZE - 1) / _BLOCKSIZE 115 if newblocks <= oldblocks: 116 pos, siz = self._setval(pos, val) 117 self._index[key] = pos, siz 118 else: 119 pos, siz = self._addval(val) 120 self._index[key] = pos, siz 121 self._addkey(key, (pos, siz)) 122 123 def __delitem__(self, key): 124 del self._index[key] 125 self._commit() 126 127 def keys(self): 128 return self._index.keys() 129 130 def has_key(self, key): 131 return self._index.has_key(key) 132 133 def __len__(self): 134 return len(self._index) 135 136 def close(self): 137 self._index = None 138 self._datfile = self._dirfile = self._bakfile = None 139 140 141def open(file, flag = None, mode = None): 142 # flag, mode arguments are currently ignored 143 return _Database(file) 144