dirdbm.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. # -*- test-case-name: twisted.test.test_dirdbm -*-
  2. #
  3. # Copyright (c) Twisted Matrix Laboratories.
  4. # See LICENSE for details.
  5. """
  6. DBM-style interface to a directory.
  7. Each key is stored as a single file. This is not expected to be very fast or
  8. efficient, but it's good for easy debugging.
  9. DirDBMs are *not* thread-safe, they should only be accessed by one thread at
  10. a time.
  11. No files should be placed in the working directory of a DirDBM save those
  12. created by the DirDBM itself!
  13. Maintainer: Itamar Shtull-Trauring
  14. """
  15. import os
  16. import base64
  17. import glob
  18. try:
  19. import cPickle as pickle
  20. except ImportError:
  21. import pickle
  22. from twisted.python.filepath import FilePath
  23. try:
  24. _open
  25. except NameError:
  26. _open = open
  27. class DirDBM:
  28. """
  29. A directory with a DBM interface.
  30. This class presents a hash-like interface to a directory of small,
  31. flat files. It can only use strings as keys or values.
  32. """
  33. def __init__(self, name):
  34. """
  35. @type name: str
  36. @param name: Base path to use for the directory storage.
  37. """
  38. self.dname = os.path.abspath(name)
  39. self._dnamePath = FilePath(name)
  40. if not self._dnamePath.isdir():
  41. self._dnamePath.createDirectory()
  42. else:
  43. # Run recovery, in case we crashed. we delete all files ending
  44. # with ".new". Then we find all files who end with ".rpl". If a
  45. # corresponding file exists without ".rpl", we assume the write
  46. # failed and delete the ".rpl" file. If only a ".rpl" exist we
  47. # assume the program crashed right after deleting the old entry
  48. # but before renaming the replacement entry.
  49. #
  50. # NOTE: '.' is NOT in the base64 alphabet!
  51. for f in glob.glob(self._dnamePath.child("*.new").path):
  52. os.remove(f)
  53. replacements = glob.glob(self._dnamePath.child("*.rpl").path)
  54. for f in replacements:
  55. old = f[:-4]
  56. if os.path.exists(old):
  57. os.remove(f)
  58. else:
  59. os.rename(f, old)
  60. def _encode(self, k):
  61. """
  62. Encode a key so it can be used as a filename.
  63. """
  64. # NOTE: '_' is NOT in the base64 alphabet!
  65. return base64.encodestring(k).replace(b'\n', b'_').replace(b"/", b"-")
  66. def _decode(self, k):
  67. """
  68. Decode a filename to get the key.
  69. """
  70. return base64.decodestring(k.replace(b'_', b'\n').replace(b"-", b"/"))
  71. def _readFile(self, path):
  72. """
  73. Read in the contents of a file.
  74. Override in subclasses to e.g. provide transparently encrypted dirdbm.
  75. """
  76. with _open(path.path, "rb") as f:
  77. s = f.read()
  78. return s
  79. def _writeFile(self, path, data):
  80. """
  81. Write data to a file.
  82. Override in subclasses to e.g. provide transparently encrypted dirdbm.
  83. """
  84. with _open(path.path, "wb") as f:
  85. f.write(data)
  86. f.flush()
  87. def __len__(self):
  88. """
  89. @return: The number of key/value pairs in this Shelf
  90. """
  91. return len(self._dnamePath.listdir())
  92. def __setitem__(self, k, v):
  93. """
  94. C{dirdbm[k] = v}
  95. Create or modify a textfile in this directory
  96. @type k: bytes
  97. @param k: key to set
  98. @type v: bytes
  99. @param v: value to associate with C{k}
  100. """
  101. if not type(k) == bytes:
  102. raise TypeError("DirDBM key must be bytes")
  103. if not type(v) == bytes:
  104. raise TypeError("DirDBM value must be bytes")
  105. k = self._encode(k)
  106. # We create a new file with extension .new, write the data to it, and
  107. # if the write succeeds delete the old file and rename the new one.
  108. old = self._dnamePath.child(k)
  109. if old.exists():
  110. new = old.siblingExtension(".rpl") # Replacement entry
  111. else:
  112. new = old.siblingExtension(".new") # New entry
  113. try:
  114. self._writeFile(new, v)
  115. except:
  116. new.remove()
  117. raise
  118. else:
  119. if (old.exists()): old.remove()
  120. new.moveTo(old)
  121. def __getitem__(self, k):
  122. """
  123. C{dirdbm[k]}
  124. Get the contents of a file in this directory as a string.
  125. @type k: bytes
  126. @param k: key to lookup
  127. @return: The value associated with C{k}
  128. @raise KeyError: Raised when there is no such key
  129. """
  130. if not type(k) == bytes:
  131. raise TypeError("DirDBM key must be bytes")
  132. path = self._dnamePath.child(self._encode(k))
  133. try:
  134. return self._readFile(path)
  135. except (EnvironmentError):
  136. raise KeyError(k)
  137. def __delitem__(self, k):
  138. """
  139. C{del dirdbm[foo]}
  140. Delete a file in this directory.
  141. @type k: bytes
  142. @param k: key to delete
  143. @raise KeyError: Raised when there is no such key
  144. """
  145. if not type(k) == bytes:
  146. raise TypeError("DirDBM key must be bytes")
  147. k = self._encode(k)
  148. try:
  149. self._dnamePath.child(k).remove()
  150. except (EnvironmentError):
  151. raise KeyError(self._decode(k))
  152. def keys(self):
  153. """
  154. @return: a L{list} of filenames (keys).
  155. """
  156. return list(map(self._decode, self._dnamePath.asBytesMode().listdir()))
  157. def values(self):
  158. """
  159. @return: a L{list} of file-contents (values).
  160. """
  161. vals = []
  162. keys = self.keys()
  163. for key in keys:
  164. vals.append(self[key])
  165. return vals
  166. def items(self):
  167. """
  168. @return: a L{list} of 2-tuples containing key/value pairs.
  169. """
  170. items = []
  171. keys = self.keys()
  172. for key in keys:
  173. items.append((key, self[key]))
  174. return items
  175. def has_key(self, key):
  176. """
  177. @type key: bytes
  178. @param key: The key to test
  179. @return: A true value if this dirdbm has the specified key, a false
  180. value otherwise.
  181. """
  182. if not type(key) == bytes:
  183. raise TypeError("DirDBM key must be bytes")
  184. key = self._encode(key)
  185. return self._dnamePath.child(key).isfile()
  186. def setdefault(self, key, value):
  187. """
  188. @type key: bytes
  189. @param key: The key to lookup
  190. @param value: The value to associate with key if key is not already
  191. associated with a value.
  192. """
  193. if key not in self:
  194. self[key] = value
  195. return value
  196. return self[key]
  197. def get(self, key, default = None):
  198. """
  199. @type key: bytes
  200. @param key: The key to lookup
  201. @param default: The value to return if the given key does not exist
  202. @return: The value associated with C{key} or C{default} if not
  203. L{DirDBM.has_key(key)}
  204. """
  205. if key in self:
  206. return self[key]
  207. else:
  208. return default
  209. def __contains__(self, key):
  210. """
  211. @see: L{DirDBM.has_key}
  212. """
  213. return self.has_key(key)
  214. def update(self, dict):
  215. """
  216. Add all the key/value pairs in L{dict} to this dirdbm. Any conflicting
  217. keys will be overwritten with the values from L{dict}.
  218. @type dict: mapping
  219. @param dict: A mapping of key/value pairs to add to this dirdbm.
  220. """
  221. for key, val in dict.items():
  222. self[key]=val
  223. def copyTo(self, path):
  224. """
  225. Copy the contents of this dirdbm to the dirdbm at C{path}.
  226. @type path: L{str}
  227. @param path: The path of the dirdbm to copy to. If a dirdbm
  228. exists at the destination path, it is cleared first.
  229. @rtype: C{DirDBM}
  230. @return: The dirdbm this dirdbm was copied to.
  231. """
  232. path = FilePath(path)
  233. assert path != self._dnamePath
  234. d = self.__class__(path.path)
  235. d.clear()
  236. for k in self.keys():
  237. d[k] = self[k]
  238. return d
  239. def clear(self):
  240. """
  241. Delete all key/value pairs in this dirdbm.
  242. """
  243. for k in self.keys():
  244. del self[k]
  245. def close(self):
  246. """
  247. Close this dbm: no-op, for dbm-style interface compliance.
  248. """
  249. def getModificationTime(self, key):
  250. """
  251. Returns modification time of an entry.
  252. @return: Last modification date (seconds since epoch) of entry C{key}
  253. @raise KeyError: Raised when there is no such key
  254. """
  255. if not type(key) == bytes:
  256. raise TypeError("DirDBM key must be bytes")
  257. path = self._dnamePath.child(self._encode(key))
  258. if path.isfile():
  259. return path.getModificationTime()
  260. else:
  261. raise KeyError(key)
  262. class Shelf(DirDBM):
  263. """
  264. A directory with a DBM shelf interface.
  265. This class presents a hash-like interface to a directory of small,
  266. flat files. Keys must be strings, but values can be any given object.
  267. """
  268. def __setitem__(self, k, v):
  269. """
  270. C{shelf[foo] = bar}
  271. Create or modify a textfile in this directory.
  272. @type k: str
  273. @param k: The key to set
  274. @param v: The value to associate with C{key}
  275. """
  276. v = pickle.dumps(v)
  277. DirDBM.__setitem__(self, k, v)
  278. def __getitem__(self, k):
  279. """
  280. C{dirdbm[foo]}
  281. Get and unpickle the contents of a file in this directory.
  282. @type k: bytes
  283. @param k: The key to lookup
  284. @return: The value associated with the given key
  285. @raise KeyError: Raised if the given key does not exist
  286. """
  287. return pickle.loads(DirDBM.__getitem__(self, k))
  288. def open(file, flag = None, mode = None):
  289. """
  290. This is for 'anydbm' compatibility.
  291. @param file: The parameter to pass to the DirDBM constructor.
  292. @param flag: ignored
  293. @param mode: ignored
  294. """
  295. return DirDBM(file)
  296. __all__ = ["open", "DirDBM", "Shelf"]