dumbdbm.py revision 22a18904e43c47902ae1e58922abeb87b530b0ae
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
31error = IOError				# For anydbm
32
33class _Database:
34
35	def __init__(self, file):
36		self._dirfile = file + '.dir'
37		self._datfile = file + '.dat'
38		self._bakfile = file + '.bak'
39		# Mod by Jack: create data file if needed
40		try:
41			f = _open(self._datfile, 'r')
42		except IOError:
43			f = _open(self._datfile, 'w')
44		f.close()
45		self._update()
46
47	def _update(self):
48		self._index = {}
49		try:
50			f = _open(self._dirfile)
51		except IOError:
52			pass
53		else:
54			while 1:
55				line = f.readline()
56				if not line: break
57				key, (pos, siz) = eval(line)
58				self._index[key] = (pos, siz)
59			f.close()
60
61	def _commit(self):
62		try: _os.unlink(self._bakfile)
63		except _os.error: pass
64		try: _os.rename(self._dirfile, self._bakfile)
65		except _os.error: pass
66		f = _open(self._dirfile, 'w')
67		for key, (pos, siz) in self._index.items():
68			f.write("%s, (%s, %s)\n" % (`key`, `pos`, `siz`))
69		f.close()
70
71	def __getitem__(self, key):
72		pos, siz = self._index[key]	# may raise KeyError
73		f = _open(self._datfile, 'rb')
74		f.seek(pos)
75		dat = f.read(siz)
76		f.close()
77		return dat
78
79	def _addval(self, val):
80		f = _open(self._datfile, 'rb+')
81		f.seek(0, 2)
82		pos = f.tell()
83## Does not work under MW compiler
84##		pos = ((pos + _BLOCKSIZE - 1) / _BLOCKSIZE) * _BLOCKSIZE
85##		f.seek(pos)
86		npos = ((pos + _BLOCKSIZE - 1) / _BLOCKSIZE) * _BLOCKSIZE
87		f.write('\0'*(npos-pos))
88		pos = npos
89
90		f.write(val)
91		f.close()
92		return (pos, len(val))
93
94	def _setval(self, pos, val):
95		f = _open(self._datfile, 'rb+')
96		f.seek(pos)
97		f.write(val)
98		f.close()
99		return (pos, len(val))
100
101	def _addkey(self, key, (pos, siz)):
102		self._index[key] = (pos, siz)
103		f = _open(self._dirfile, 'a')
104		f.write("%s, (%s, %s)\n" % (`key`, `pos`, `siz`))
105		f.close()
106
107	def __setitem__(self, key, val):
108		if not type(key) == type('') == type(val):
109			raise TypeError, "keys and values must be strings"
110		if not self._index.has_key(key):
111			(pos, siz) = self._addval(val)
112			self._addkey(key, (pos, siz))
113		else:
114			pos, siz = self._index[key]
115			oldblocks = (siz + _BLOCKSIZE - 1) / _BLOCKSIZE
116			newblocks = (len(val) + _BLOCKSIZE - 1) / _BLOCKSIZE
117			if newblocks <= oldblocks:
118				pos, siz = self._setval(pos, val)
119				self._index[key] = pos, siz
120			else:
121				pos, siz = self._addval(val)
122				self._index[key] = pos, siz
123			self._addkey(key, (pos, siz))
124
125	def __delitem__(self, key):
126		del self._index[key]
127		self._commit()
128
129	def keys(self):
130		return self._index.keys()
131
132	def has_key(self, key):
133		return self._index.has_key(key)
134
135	def __len__(self):
136		return len(self._index)
137
138	def close(self):
139		self._index = None
140		self._datfile = self._dirfile = self._bakfile = None
141
142
143def open(file, flag = None, mode = None):
144	# flag, mode arguments are currently ignored
145	return _Database(file)
146