From 219b489d0117e8dbbb88a84fd446e2ab6f965e3a Mon Sep 17 00:00:00 2001 From: Galymzhan Date: Mon, 17 Dec 2012 01:19:16 +0600 Subject: [PATCH] add size-based limit for files cache, fixes #1321 --- src/Composer/Cache.php | 20 ++++- src/Composer/Config.php | 20 +++++ src/Composer/Downloader/FileDownloader.php | 2 +- tests/Composer/Test/CacheTest.php | 94 ++++++++++++++++++++++ 4 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 tests/Composer/Test/CacheTest.php diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php index a3b9114ba..d2249127c 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -114,16 +114,27 @@ class Cache return false; } - public function gc($ttl) + public function gc($ttl, $cacheMaxSize) { $expire = new \DateTime(); $expire->modify('-'.$ttl.' seconds'); - $finder = Finder::create()->files()->in($this->root)->date('until '.$expire->format('Y-m-d H:i:s')); + $finder = $this->getFinder()->date('until '.$expire->format('Y-m-d H:i:s')); foreach ($finder as $file) { unlink($file->getRealPath()); } + $totalCacheSize = $this->filesystem->size($this->root); + if ($totalCacheSize > $cacheMaxSize) { + $iterator = $this->getFinder()->sortByAccessedTime()->getIterator(); + while ($totalCacheSize > $cacheMaxSize && $iterator->valid()) { + $filepath = $iterator->current()->getRealPath(); + $totalCacheSize -= $this->filesystem->size($filepath); + unlink($filepath); + $iterator->next(); + } + } + return true; } @@ -146,4 +157,9 @@ class Cache return false; } + + protected function getFinder() + { + return Finder::create()->in($this->root)->files(); + } } diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 53f93faff..23be57468 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -22,6 +22,7 @@ class Config public static $defaultConfig = array( 'process-timeout' => 300, 'cache-ttl' => 15552000, // 6 months + 'cache-files-maxsize' => '300MiB', 'vendor-dir' => 'vendor', 'bin-dir' => '{$vendor-dir}/bin', 'notify-on-install' => true, @@ -137,6 +138,25 @@ class Config case 'cache-ttl': return (int) $this->config[$key]; + case 'cache-files-maxsize': + if (!preg_match('/^\s*(\d+)\s*([kmg]ib)?\s*$/i', $this->config[$key], $matches)) { + throw new \RuntimeException( + "composer.json contains invalid 'cache-files-maxsize' value: {$this->config[$key]}" + ); + } + $size = $matches[1]; + if (isset($matches[2])) { + switch (strtolower($matches[2])) { + case 'gib': + $size *= 1024; + case 'mib': + $size *= 1024; + case 'kib': + $size *= 1024; + } + } + return $size; + case 'cache-files-ttl': if (isset($this->config[$key])) { return (int) $this->config[$key]; diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 7f8d8e0b6..2437fd635 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -56,7 +56,7 @@ class FileDownloader implements DownloaderInterface $this->cache = $cache; if ($this->cache && !self::$cacheCollected && !rand(0, 50)) { - $this->cache->gc($config->get('cache-ttl')); + $this->cache->gc($config->get('cache-ttl'), $config->get('cache-files-maxsize')); } self::$cacheCollected = true; } diff --git a/tests/Composer/Test/CacheTest.php b/tests/Composer/Test/CacheTest.php new file mode 100644 index 000000000..a07f35e2f --- /dev/null +++ b/tests/Composer/Test/CacheTest.php @@ -0,0 +1,94 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test; + +use Composer\Cache; + +class CacheTest extends TestCase +{ + private $files, $root, $finder, $cache; + + public function setUp() + { + $this->root = sys_get_temp_dir() . '/composer_testdir'; + $this->ensureDirectoryExistsAndClear($this->root); + + $this->files = array(); + $zeros = str_repeat('0', 1000); + for ($i = 0; $i < 4; $i++) { + file_put_contents("{$this->root}/cached.file{$i}.zip", $zeros); + $this->files[] = new \SplFileInfo("{$this->root}/cached.file{$i}.zip"); + } + $this->finder = $this->getMock('Symfony\Component\Finder\Finder'); + + $io = $this->getMock('Composer\IO\IOInterface'); + $this->cache = $this->getMock( + 'Composer\Cache', + array('getFinder'), + array($io, $this->root) + ); + $this->cache + ->expects($this->any()) + ->method('getFinder') + ->will($this->returnValue($this->finder)); + } + + public function testRemoveOutdatedFiles() + { + $outdated = array_slice($this->files, 1); + $this->finder + ->expects($this->once()) + ->method('getIterator') + ->will($this->returnValue(new \ArrayIterator($outdated))); + $this->finder + ->expects($this->once()) + ->method('date') + ->will($this->returnValue($this->finder)); + + $this->cache->gc(600, 1024 * 1024 * 1024); + + for ($i = 1; $i < 4; $i++) { + $this->assertFileNotExists("{$this->root}/cached.file{$i}.zip"); + } + $this->assertFileExists("{$this->root}/cached.file0.zip"); + } + + public function testRemoveFilesWhenCacheIsTooLarge() + { + $emptyFinder = $this->getMock('Symfony\Component\Finder\Finder'); + $emptyFinder + ->expects($this->once()) + ->method('getIterator') + ->will($this->returnValue(new \EmptyIterator())); + + $this->finder + ->expects($this->once()) + ->method('date') + ->will($this->returnValue($emptyFinder)); + $this->finder + ->expects($this->once()) + ->method('getIterator') + ->will($this->returnValue(new \ArrayIterator($this->files))); + $this->finder + ->expects($this->once()) + ->method('sortByAccessedTime') + ->will($this->returnValue($this->finder)); + + $this->cache->gc(600, 1500); + + for ($i = 0; $i < 3; $i++) { + $this->assertFileNotExists("{$this->root}/cached.file{$i}.zip"); + } + $this->assertFileExists("{$this->root}/cached.file3.zip"); + } +}