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