blob: e35673387930c0b4ff1afc022257b9406025ac93 [file] [log] [blame]
Guido van Rossum9f824a71995-08-10 19:29:28 +00001"""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()
Guido van Rossumba426641996-01-25 18:35:24 +000097 return (pos, len(val))
Guido van Rossum9f824a71995-08-10 19:29:28 +000098
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)