Django

Code

Changeset 6887

Show
Ignore:
Timestamp:
12/04/07 12:03:56 (9 months ago)
Author:
jacob
Message:

Fixed #6099: the filebased cache backend now uses md5 hashes of keys instead of sanitized filenames. For good measure, keys are partitioned into subdirectories using the first few bits of the hash. Thanks, sherbang.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/core/cache/backends/filebased.py

    r6822 r6887  
    11"File-based cache backend" 
    22 
     3import md5 
    34import os, time 
    45try: 
     
    78    import pickle 
    89from django.core.cache.backends.base import BaseCache 
    9 from django.utils.http import urlquote_plus 
    1010 
    1111class CacheClass(BaseCache): 
     
    3030 
    3131    def add(self, key, value, timeout=None): 
    32         fname = self._key_to_file(key) 
    33         if timeout is None: 
    34             timeout = self.default_timeout 
    35         try: 
    36             filelist = os.listdir(self._dir) 
    37         except (IOError, OSError): 
    38             self._createdir() 
    39             filelist = [] 
    40         if len(filelist) > self._max_entries: 
    41             self._cull(filelist) 
    42         if os.path.basename(fname) not in filelist: 
    43             try: 
    44                 f = open(fname, 'wb') 
    45                 now = time.time() 
    46                 pickle.dump(now + timeout, f, 2) 
    47                 pickle.dump(value, f, 2) 
    48             except (IOError, OSError): 
    49                 pass 
     32        if self.has_key(key): 
     33            return None 
     34         
     35        self.set(key, value, timeout) 
    5036 
    5137    def get(self, key, default=None): 
     
    5743            if exp < now: 
    5844                f.close() 
    59                 os.remove(fname) 
     45                self._delete(fname) 
    6046            else: 
    6147                return pickle.load(f) 
     
    6652    def set(self, key, value, timeout=None): 
    6753        fname = self._key_to_file(key) 
     54        dirname = os.path.dirname(fname) 
     55         
    6856        if timeout is None: 
    6957            timeout = self.default_timeout 
     58             
     59        self._cull() 
     60         
    7061        try: 
    71             filelist = os.listdir(self._dir) 
    72         except (IOError, OSError): 
    73             self._createdir() 
    74             filelist = [] 
    75         if len(filelist) > self._max_entries: 
    76             self._cull(filelist) 
    77         try: 
     62            if not os.path.exists(dirname): 
     63                os.makedirs(dirname) 
     64 
    7865            f = open(fname, 'wb') 
    7966            now = time.time() 
    80             pickle.dump(now + timeout, f, 2
    81             pickle.dump(value, f, 2
     67            pickle.dump(now + timeout, f, pickle.HIGHEST_PROTOCOL
     68            pickle.dump(value, f, pickle.HIGHEST_PROTOCOL
    8269        except (IOError, OSError): 
    8370            pass 
     
    8572    def delete(self, key): 
    8673        try: 
    87             os.remove(self._key_to_file(key)) 
     74            self._delete(self._key_to_file(key)) 
     75        except (IOError, OSError): 
     76            pass 
     77 
     78    def _delete(self, fname): 
     79        os.remove(fname) 
     80        try: 
     81            # Remove the 2 subdirs if they're empty 
     82            dirname = os.path.dirname(fname) 
     83            os.rmdir(dirname) 
     84            os.rmdir(os.path.dirname(dirname)) 
    8885        except (IOError, OSError): 
    8986            pass 
    9087 
    9188    def has_key(self, key): 
    92         return os.path.exists(self._key_to_file(key)) 
     89        fname = self._key_to_file(key) 
     90        try: 
     91            f = open(fname, 'rb') 
     92            exp = pickle.load(f) 
     93            now = time.time() 
     94            if exp < now: 
     95                f.close() 
     96                self._delete(fname) 
     97                return False 
     98            else: 
     99                return True 
     100        except (IOError, OSError, EOFError, pickle.PickleError): 
     101            return False 
    93102 
    94     def _cull(self, filelist): 
     103    def _cull(self): 
     104        if int(self._num_entries) < self._max_entries: 
     105            return 
     106         
     107        try: 
     108            filelist = os.listdir(self._dir) 
     109        except (IOError, OSError): 
     110            return 
     111         
    95112        if self._cull_frequency == 0: 
    96113            doomed = filelist 
    97114        else: 
    98             doomed = [k for (i, k) in enumerate(filelist) if i % self._cull_frequency == 0] 
    99         for fname in doomed: 
     115            doomed = [os.path.join(self._dir, k) for (i, k) in enumerate(filelist) if i % self._cull_frequency == 0] 
     116 
     117        for topdir in doomed: 
    100118            try: 
    101                 os.remove(os.path.join(self._dir, fname)) 
     119                for root, _, files in os.walk(topdir): 
     120                    for f in files: 
     121                        self._delete(os.path.join(root, f)) 
    102122            except (IOError, OSError): 
    103123                pass 
     
    110130 
    111131    def _key_to_file(self, key): 
    112         return os.path.join(self._dir, urlquote_plus(key)) 
     132        """ 
     133        Convert the filename into an md5 string. We'll turn the first couple 
     134        bits of the path into directory prefixes to be nice to filesystems 
     135        that have problems with large numbers of files in a directory. 
     136         
     137        Thus, a cache key of "foo" gets turnned into a file named 
     138        ``{cache-dir}ac/bd/18db4cc2f85cedef654fccc4a4d8``. 
     139        """ 
     140        path = md5.new(key.encode('utf-8')).hexdigest() 
     141        path = os.path.join(path[:2], path[2:4], path[4:]) 
     142        return os.path.join(self._dir, path) 
     143 
     144    def _get_num_entries(self): 
     145        count = 0 
     146        for _,_,files in os.walk(self._dir): 
     147            count += len(files) 
     148        return count 
     149    _num_entries = property(_get_num_entries) 
     150 
  • django/trunk/tests/regressiontests/cache/tests.py

    r6822 r6887  
    44# Uses whatever cache backend is set in the test settings file. 
    55 
    6 import time, unittest 
    7  
     6import time 
     7import unittest 
    88from django.core.cache import cache 
    99from django.utils.cache import patch_vary_headers 
     
    2828        cache.add("addkey1", "newvalue") 
    2929        self.assertEqual(cache.get("addkey1"), "value") 
    30  
     30         
    3131    def test_non_existent(self): 
    3232        # get with non-existent keys 
     
    7777 
    7878    def test_expiration(self): 
    79         # expiration 
    80         cache.set('expire', 'very quickly', 1) 
    81         time.sleep(2) 
    82         self.assertEqual(cache.get("expire"), None) 
     79        cache.set('expire1', 'very quickly', 1) 
     80        cache.set('expire2', 'very quickly', 1) 
     81        cache.set('expire3', 'very quickly', 1) 
     82 
     83        time.sleep(2)         
     84        self.assertEqual(cache.get("expire1"), None) 
     85         
     86        cache.add("expire2", "newvalue") 
     87        self.assertEqual(cache.get("expire2"), "newvalue") 
     88        self.assertEqual(cache.has_key("expire3"), False) 
    8389 
    8490    def test_unicode(self): 
     
    9399            self.assertEqual(cache.get(key), value) 
    94100 
     101import os 
     102import md5 
     103import shutil 
     104import tempfile 
     105from django.core.cache.backends.filebased import CacheClass as FileCache 
     106 
     107class FileBasedCacheTests(unittest.TestCase): 
     108    """ 
     109    Specific test cases for the file-based cache. 
     110    """ 
     111    def setUp(self): 
     112        self.dirname = tempfile.mktemp() 
     113        os.mkdir(self.dirname) 
     114        self.cache = FileCache(self.dirname, {}) 
     115         
     116    def tearDown(self): 
     117        shutil.rmtree(self.dirname) 
     118         
     119    def test_hashing(self): 
     120        """Test that keys are hashed into subdirectories correctly""" 
     121        self.cache.set("foo", "bar") 
     122        keyhash = md5.new("foo").hexdigest() 
     123        keypath = os.path.join(self.dirname, keyhash[:2], keyhash[2:4], keyhash[4:]) 
     124        self.assert_(os.path.exists(keypath)) 
     125         
     126    def test_subdirectory_removal(self): 
     127        """ 
     128        Make sure that the created subdirectories are correctly removed when empty. 
     129        """ 
     130        self.cache.set("foo", "bar") 
     131        keyhash = md5.new("foo").hexdigest() 
     132        keypath = os.path.join(self.dirname, keyhash[:2], keyhash[2:4], keyhash[4:]) 
     133        self.assert_(os.path.exists(keypath)) 
     134 
     135        self.cache.delete("foo") 
     136        self.assert_(not os.path.exists(keypath)) 
     137        self.assert_(not os.path.exists(os.path.dirname(keypath))) 
     138        self.assert_(not os.path.exists(os.path.dirname(os.path.dirname(keypath)))) 
    95139 
    96140class CacheUtils(unittest.TestCase):