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