You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

filebased.py 5.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. "File-based cache backend"
  2. import glob
  3. import hashlib
  4. import os
  5. import pickle
  6. import random
  7. import tempfile
  8. import time
  9. import zlib
  10. from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache
  11. from django.core.files import locks
  12. from django.core.files.move import file_move_safe
  13. class FileBasedCache(BaseCache):
  14. cache_suffix = '.djcache'
  15. pickle_protocol = pickle.HIGHEST_PROTOCOL
  16. def __init__(self, dir, params):
  17. super().__init__(params)
  18. self._dir = os.path.abspath(dir)
  19. self._createdir()
  20. def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
  21. if self.has_key(key, version):
  22. return False
  23. self.set(key, value, timeout, version)
  24. return True
  25. def get(self, key, default=None, version=None):
  26. fname = self._key_to_file(key, version)
  27. try:
  28. with open(fname, 'rb') as f:
  29. if not self._is_expired(f):
  30. return pickle.loads(zlib.decompress(f.read()))
  31. except FileNotFoundError:
  32. pass
  33. return default
  34. def _write_content(self, file, timeout, value):
  35. expiry = self.get_backend_timeout(timeout)
  36. file.write(pickle.dumps(expiry, self.pickle_protocol))
  37. file.write(zlib.compress(pickle.dumps(value, self.pickle_protocol)))
  38. def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
  39. self._createdir() # Cache dir can be deleted at any time.
  40. fname = self._key_to_file(key, version)
  41. self._cull() # make some room if necessary
  42. fd, tmp_path = tempfile.mkstemp(dir=self._dir)
  43. renamed = False
  44. try:
  45. with open(fd, 'wb') as f:
  46. self._write_content(f, timeout, value)
  47. file_move_safe(tmp_path, fname, allow_overwrite=True)
  48. renamed = True
  49. finally:
  50. if not renamed:
  51. os.remove(tmp_path)
  52. def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
  53. try:
  54. with open(self._key_to_file(key, version), 'r+b') as f:
  55. try:
  56. locks.lock(f, locks.LOCK_EX)
  57. if self._is_expired(f):
  58. return False
  59. else:
  60. previous_value = pickle.loads(zlib.decompress(f.read()))
  61. f.seek(0)
  62. self._write_content(f, timeout, previous_value)
  63. return True
  64. finally:
  65. locks.unlock(f)
  66. except FileNotFoundError:
  67. return False
  68. def delete(self, key, version=None):
  69. self._delete(self._key_to_file(key, version))
  70. def _delete(self, fname):
  71. if not fname.startswith(self._dir) or not os.path.exists(fname):
  72. return
  73. try:
  74. os.remove(fname)
  75. except FileNotFoundError:
  76. # The file may have been removed by another process.
  77. pass
  78. def has_key(self, key, version=None):
  79. fname = self._key_to_file(key, version)
  80. if os.path.exists(fname):
  81. with open(fname, 'rb') as f:
  82. return not self._is_expired(f)
  83. return False
  84. def _cull(self):
  85. """
  86. Remove random cache entries if max_entries is reached at a ratio
  87. of num_entries / cull_frequency. A value of 0 for CULL_FREQUENCY means
  88. that the entire cache will be purged.
  89. """
  90. filelist = self._list_cache_files()
  91. num_entries = len(filelist)
  92. if num_entries < self._max_entries:
  93. return # return early if no culling is required
  94. if self._cull_frequency == 0:
  95. return self.clear() # Clear the cache when CULL_FREQUENCY = 0
  96. # Delete a random selection of entries
  97. filelist = random.sample(filelist,
  98. int(num_entries / self._cull_frequency))
  99. for fname in filelist:
  100. self._delete(fname)
  101. def _createdir(self):
  102. if not os.path.exists(self._dir):
  103. try:
  104. os.makedirs(self._dir, 0o700)
  105. except FileExistsError:
  106. pass
  107. def _key_to_file(self, key, version=None):
  108. """
  109. Convert a key into a cache file path. Basically this is the
  110. root cache path joined with the md5sum of the key and a suffix.
  111. """
  112. key = self.make_key(key, version=version)
  113. self.validate_key(key)
  114. return os.path.join(self._dir, ''.join(
  115. [hashlib.md5(key.encode()).hexdigest(), self.cache_suffix]))
  116. def clear(self):
  117. """
  118. Remove all the cache files.
  119. """
  120. if not os.path.exists(self._dir):
  121. return
  122. for fname in self._list_cache_files():
  123. self._delete(fname)
  124. def _is_expired(self, f):
  125. """
  126. Take an open cache file `f` and delete it if it's expired.
  127. """
  128. try:
  129. exp = pickle.load(f)
  130. except EOFError:
  131. exp = 0 # An empty file is considered expired.
  132. if exp is not None and exp < time.time():
  133. f.close() # On Windows a file has to be closed before deleting
  134. self._delete(f.name)
  135. return True
  136. return False
  137. def _list_cache_files(self):
  138. """
  139. Get a list of paths to all the cache files. These are all the files
  140. in the root cache dir that end on the cache_suffix.
  141. """
  142. if not os.path.exists(self._dir):
  143. return []
  144. filelist = [os.path.join(self._dir, fname) for fname
  145. in glob.glob1(self._dir, '*%s' % self.cache_suffix)]
  146. return filelist