瀏覽代碼

Merge remote-tracking branch 'galymzhan/add-cache-files-maxsize'

Jordi Boggiano 12 年之前
父節點
當前提交
6ce285b70c

+ 18 - 2
src/Composer/Cache.php

@@ -114,16 +114,27 @@ class Cache
         return false;
         return false;
     }
     }
 
 
-    public function gc($ttl)
+    public function gc($ttl, $cacheMaxSize)
     {
     {
         $expire = new \DateTime();
         $expire = new \DateTime();
         $expire->modify('-'.$ttl.' seconds');
         $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) {
         foreach ($finder as $file) {
             unlink($file->getRealPath());
             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;
         return true;
     }
     }
 
 
@@ -146,4 +157,9 @@ class Cache
 
 
         return false;
         return false;
     }
     }
+
+    protected function getFinder()
+    {
+        return Finder::create()->in($this->root)->files();
+    }
 }
 }

+ 20 - 0
src/Composer/Config.php

@@ -22,6 +22,7 @@ class Config
     public static $defaultConfig = array(
     public static $defaultConfig = array(
         'process-timeout' => 300,
         'process-timeout' => 300,
         'cache-ttl' => 15552000, // 6 months
         'cache-ttl' => 15552000, // 6 months
+        'cache-files-maxsize' => '300MiB',
         'vendor-dir' => 'vendor',
         'vendor-dir' => 'vendor',
         'bin-dir' => '{$vendor-dir}/bin',
         'bin-dir' => '{$vendor-dir}/bin',
         'notify-on-install' => true,
         'notify-on-install' => true,
@@ -137,6 +138,25 @@ class Config
             case 'cache-ttl':
             case 'cache-ttl':
                 return (int) $this->config[$key];
                 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':
             case 'cache-files-ttl':
                 if (isset($this->config[$key])) {
                 if (isset($this->config[$key])) {
                     return (int) $this->config[$key];
                     return (int) $this->config[$key];

+ 1 - 1
src/Composer/Downloader/FileDownloader.php

@@ -56,7 +56,7 @@ class FileDownloader implements DownloaderInterface
         $this->cache = $cache;
         $this->cache = $cache;
 
 
         if ($this->cache && !self::$cacheCollected && !rand(0, 50)) {
         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;
         self::$cacheCollected = true;
     }
     }

+ 32 - 0
src/Composer/Util/Filesystem.php

@@ -269,6 +269,38 @@ class Filesystem
         return substr($path, 0, 1) === '/' || substr($path, 1, 1) === ':';
         return substr($path, 0, 1) === '/' || substr($path, 1, 1) === ':';
     }
     }
 
 
+    /**
+     * Returns size of a file or directory specified by path. If a directory is
+     * given, it's size will be computed recursively.
+     *
+     * @param  string $path Path to the file or directory
+     * @return int
+     */
+    public function size($path)
+    {
+        if (!file_exists($path)) {
+            throw new \RuntimeException("$path does not exist.");
+        }
+        if (is_dir($path)) {
+            return $this->directorySize($path);
+        }
+        return filesize($path);
+    }
+
+    protected function directorySize($directory)
+    {
+        $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS);
+        $ri = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
+
+        $size = 0;
+        foreach ($ri as $file) {
+            if ($file->isFile()) {
+                $size += $file->getSize();
+            }
+        }
+        return $size;
+    }
+
     protected function getProcess()
     protected function getProcess()
     {
     {
         return new ProcessExecutor;
         return new ProcessExecutor;

+ 94 - 0
tests/Composer/Test/CacheTest.php

@@ -0,0 +1,94 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * 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");
+    }
+}

+ 20 - 0
tests/Composer/Test/Util/FilesystemTest.php

@@ -107,5 +107,25 @@ class FilesystemTest extends TestCase
         $this->assertTrue($fs->removeDirectoryPhp($tmp . "/composer_testdir"));
         $this->assertTrue($fs->removeDirectoryPhp($tmp . "/composer_testdir"));
         $this->assertFalse(file_exists($tmp . "/composer_testdir/level1/level2/hello.txt"));
         $this->assertFalse(file_exists($tmp . "/composer_testdir/level1/level2/hello.txt"));
     }
     }
+
+    public function testFileSize()
+    {
+        $tmp = sys_get_temp_dir();
+        file_put_contents("$tmp/composer_test_file", 'Hello');
+
+        $fs = new Filesystem;
+        $this->assertGreaterThanOrEqual(5, $fs->size("$tmp/composer_test_file"));
+    }
+
+    public function testDirectorySize()
+    {
+        $tmp = sys_get_temp_dir();
+        @mkdir("$tmp/composer_testdir", 0777, true);
+        file_put_contents("$tmp/composer_testdir/file1.txt", 'Hello');
+        file_put_contents("$tmp/composer_testdir/file2.txt", 'World');
+
+        $fs = new Filesystem;
+        $this->assertGreaterThanOrEqual(10, $fs->size("$tmp/composer_testdir"));
+    }
 }
 }