diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Store.php b/src/Symfony/Component/HttpKernel/HttpCache/Store.php index 7173f44ee3c76..e551165f95555 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Store.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Store.php @@ -16,17 +16,19 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Finder\Finder; /** * Store implements all the logic for storing cache metadata (Request and Response headers). * * @author Fabien Potencier */ -class Store implements StoreInterface +class Store implements StoreInterface, StoreHousekeepingInterface { protected $root; private $keyCache; private $locks; + private $responseContentList; /** * Constructor. @@ -41,6 +43,107 @@ public function __construct($root) } $this->keyCache = new \SplObjectStorage(); $this->locks = array(); + $this->responseContentList = array(); + } + + /** + * Deletes from disk all the stale response-header files and the response-content files. + * + * @return integer The number of the cleared entries + */ + public function clear() + { + $finder = new Finder(); + $finder->depth('< 5')->files()->notName('*.lck')->in($this->root . DIRECTORY_SEPARATOR . 'md'); + $counter = 0; + foreach ($finder as $file) { + $counter += $this->purgeKey($this->getKeyByPath($file->getPathname())); + } + foreach ($this->getResponseContentList() as $key => $isNeeded) { + if (!$isNeeded) { + $file = $this->getPath($key); + if ($this->deleteFile($file)) { + $counter++; + } + } + } + + return $counter; + } + + /** + * When the Response is stale it deletes the header file and add its response content file into the black list. + * + * @param string $key The cached key + * + * @return integer the number of file deleted + */ + protected function purgeKey($key) + { + $metadata = $this->getMetadata($key); + $expiredResponse = 0; + $contentDeleted = 0; + foreach ($metadata as $entry) { + $response = $this->restoreResponse($entry[1]); + $ResponseContentKey = $response->headers->get('x-content-digest'); + if (!$response->isFresh()) { + $this->addToResponseContentList($ResponseContentKey, false); + $expiredResponse++; + } else { + $this->addToResponseContentList($ResponseContentKey, true); + } + } + // removes the file, only if all entries are expired + if (count($metadata) == $expiredResponse) { + $file = $this->getPath($key); + if ($this->deleteFile($file, false)) { + $contentDeleted++; + } + // deletes locks + $this->deleteFile($this->getPath($key . '.lck'), false); + } + + return $contentDeleted; + } + + /** + * Add a key of a response in the ResponseContentList. + * + * @param string $key + * @param Boolean $need + * + * @return Store + */ + private function addToResponseContentList($key, $need = true) + { + if (array_key_exists($key, $this->responseContentList)) { + $this->responseContentList[$key] = $this->responseContentList[$key] || $need; + } else { + $this->responseContentList[$key] = $need; + } + + return $this; + } + + /** + * Deletes a file. + * + * @param string $file + * @param Boolean $ignoreError + * + * @return Boolean + */ + protected function deleteFile($file, $ignoreError = true) + { + if (is_file($file)) { + if ($ignoreError) { + return @unlink($file); + } + + return unlink($file); + } + + return false; } /** @@ -99,7 +202,7 @@ public function unlock(Request $request) { $file = $this->getPath($this->getCacheKey($request).'.lck'); - return is_file($file) ? @unlink($file) : false; + return $this->deleteFile($file); } public function isLocked(Request $request) @@ -368,11 +471,42 @@ private function save($key, $data) @chmod($path, 0666 & ~umask()); } + /** + * Get the Path given the cache-key. + * + * @param string $key + * + * @return string + */ public function getPath($key) { return $this->root.DIRECTORY_SEPARATOR.substr($key, 0, 2).DIRECTORY_SEPARATOR.substr($key, 2, 2).DIRECTORY_SEPARATOR.substr($key, 4, 2).DIRECTORY_SEPARATOR.substr($key, 6); } + /** + * Get the cache key given the path. + * + * @param string $path The absolute path + * + * @return string + */ + public function getKeyByPath($path) + { + $path = substr($path, strlen($this->root.DIRECTORY_SEPARATOR)); + + return str_replace(DIRECTORY_SEPARATOR, '', $path); + } + + /** + * Get the responseContentList. + * + * @return array + */ + public function getResponseContentList() + { + return $this->responseContentList; + } + /** * Returns a cache key for the given Request. * diff --git a/src/Symfony/Component/HttpKernel/HttpCache/StoreHousekeepingInterface.php b/src/Symfony/Component/HttpKernel/HttpCache/StoreHousekeepingInterface.php new file mode 100644 index 0000000000000..8623b87ffc9b0 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/HttpCache/StoreHousekeepingInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\HttpCache; + +/** + * Interface implemented by HTTP cache stores, the stores should provide a cleaner + * + * @author Giulio De Donato + * @author Fabien Potencier + */ +interface StoreHousekeepingInterface +{ + /** + * clear all the stale entries + * + * @return int the number of the cleared entries + */ + public function clear(); + +} diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php index 12d301f387af5..9f0e7837eb49c 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php @@ -219,6 +219,123 @@ public function testLocking() $this->assertFalse($this->store->isLocked($req)); } + + public function testClearSimpleEntryNotExpired() + { + $key = $this->storeSimpleEntry(); + $deletedItems = $this->store->clear(); + + $this->assertEquals($deletedItems, 0); + $this->assertFileExists($this->store->getPath($key)); + } + + + public function testClearSimpleEntryExpired() + { + $key = $this->storeSimpleEntry(); + + $this->store->invalidate($this->request); + $deletedItems = $this->store->clear(); + + $this->assertEquals($deletedItems, 2); + $this->assertFileNotExists($this->store->getPath($key)); + // the content file shouldn't exist + $r = new \ReflectionObject($this->store); + $m = $r->getMethod('generateContentDigest'); + $m->setAccessible(true); + $contentDigest = $m->invoke($this->store, $this->response); + $this->assertFileNotExists($this->store->getPath($contentDigest)); + } + + public function testClearWithOneVaryExpired() + { + $req1 = Request::create('/test', 'get', array(), array(), array(), array('HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar')); + $res1 = new Response('test 1', 200, array('Vary' => 'Foo Bar')); + $res1->setTtl(100); + $key = $this->store->write($req1, $res1); + + $req2 = Request::create('/test', 'get', array(), array(), array(), array('HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam')); + $res2 = new Response('test 2', 200, array('Vary' => 'Foo Bar')); + $res2->setTtl(0); + $key2 = $this->store->write($req2, $res2); + + $req3 = Request::create('/test', 'get', array(), array(), array(), array('HTTP_FOO' => 'Baz', 'HTTP_BAR' => 'Boom')); + $res3 = new Response('test 3', 200, array('Vary' => 'Foo Bar')); + $res3->setTtl(100); + $key3 = $this->store->write($req3, $res3); + + $deletedItems = $this->store->clear(); + + $this->assertEquals($deletedItems, 1); + $this->assertNull($this->store->lookup($req2)); + $this->assertNotNull($this->store->lookup($req1)); + $this->assertNotNull($this->store->lookup($req3)); + + $this->assertFileExists($this->store->getPath($key2)); + } + + + public function testClearSameResponseDifferentRequest() + { + $req1 = Request::create('/test', 'get', array(), array(), array(), array()); + $req2 = Request::create('/test.number-2', 'get', array(), array(), array(), array()); + $res1 = new Response('test 3', 200, array('Vary' => 'Foo Bar')); + $res1->setTtl(100); + + $key1 = $this->store->write($req1, $res1); + $key2 = $this->store->write($req2, $res1); + + $this->store->invalidate($req1); + + $deletedItems = $this->store->clear(); + + $this->assertEquals($deletedItems, 1); + $this->assertFileNotExists($this->store->getPath($key1)); + $this->assertFileExists($this->store->getPath($key2)); + } + + public function testPurgeKey() + { + $cacheKey = $this->storeSimpleEntry(); + $r = new \ReflectionObject($this->store); + $m = $r->getMethod('purgeKey'); + $m->setAccessible(true); + + $this->store->invalidate($this->request); + $this->assertEquals($m->invoke($this->store, $cacheKey), 1); + $this->assertFileNotExists($this->store->getPath($cacheKey)); + } + + public function testAddToResponseContentList() + { + $r = new \ReflectionObject($this->store); + $m = $r->getMethod('addToResponseContentList'); + $m->setAccessible(true); + $key = 'key-start-true'; + $result = $m->invoke($this->store, $key, true); + $responseList = $result->getResponseContentList(); + $this->assertTrue($responseList[$key]); + $result = $m->invoke($result, $key, false); + $responseList = $result->getResponseContentList(); + $this->assertTrue($responseList[$key]); + + $key = 'key-start-false'; + $result = $m->invoke($this->store, $key, false); + $responseList = $result->getResponseContentList(); + $this->assertFalse($responseList[$key]); + + $result = $m->invoke($this->store, $key, true); + $responseList = $result->getResponseContentList(); + $this->assertTrue($responseList[$key]); + } + + public function testGetKeyByPath() + { + $key = 'en5ed68c8380c7fd0ba2db6242f9ec06ac1dd2acf9'; + $path = $this->store->getPath($key); + $this->assertEquals($key, $this->store->getKeyByPath($path)); + } + protected function storeSimpleEntry($path = null, $headers = array()) { if (null === $path) {