diff --git a/Command/CleanMediaCommand.php b/Command/CleanMediaCommand.php new file mode 100755 index 000000000..22205252a --- /dev/null +++ b/Command/CleanMediaCommand.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\MediaBundle\Command; + +use Sonata\MediaBundle\Provider\FileProvider; +use Sonata\MediaBundle\Provider\MediaProviderInterface; +use Sonata\MediaBundle\Provider\Pool; +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Finder\Finder; + +class CleanMediaCommand extends ContainerAwareCommand +{ + /** + * @var MediaProviderInterface[]|false + */ + private $providers = false; + + /** + * {@inheritdoc} + */ + public function configure() + { + $this->setName('sonata:media:clean-uploads') + ->setDescription('Find orphaned files in media upload directory') + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Execute the cleanup as a dry run. This doesn\'t remove any files'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $dryRun = (bool) $input->getOption('dry-run'); + $verbose = (bool) $input->getOption('verbose'); + + $pool = $this->getContainer()->get('sonata.media.pool'); + $finder = Finder::create(); + $filesystem = new Filesystem(); + $baseDirectory = $this->getContainer()->get('sonata.media.adapter.filesystem.local')->getDirectory(); + + $output->writeln(sprintf('Scanning upload directory: %s', $baseDirectory)); + + foreach ($pool->getContexts() as $contextName => $context) { + if (!$filesystem->exists($baseDirectory.'/'.$contextName)) { + $output->writeln(sprintf("'%s' does not exist", $baseDirectory.'/'.$contextName)); + continue; + } + + $output->writeln(sprintf('Context: %s', $contextName)); + + $files = $finder->files()->in($baseDirectory.'/'.$contextName); + + foreach ($files as $file) { + $filename = $file->getFilename(); + + if (!$this->mediaExists($filename, $contextName)) { + if ($dryRun) { + $output->writeln(sprintf("'%s' is orphanend", $filename)); + } else { + try { + $filesystem->remove($file->getRealPath()); + $output->writeln(sprintf("'%s' was successfully removed", $filename)); + } catch (IOException $ioe) { + $output->writeln(sprintf('%s', $ioe->getMessage())); + } + } + } elseif ($verbose) { + $output->writeln(sprintf("'%s' found", $filename)); + } + } + } + + $output->writeln('done!'); + } + + /** + * @return string[] + */ + private function getProviders() + { + if (!$this->providers) { + $this->providers = array(); + + $pool = $this->getContainer()->get('sonata.media.pool'); + + foreach ($pool->getProviders() as $provider) { + if ($provider instanceof FileProvider) { + $this->providers[] = $provider->getName(); + } + } + } + + return $this->providers; + } + + /** + * @param $filename + * @param $context + * + * @return bool + */ + private function mediaExists($filename, $context = null) + { + $mediaManager = $this->getContainer()->get('sonata.media.manager.media'); + + $fileParts = explode('_', $filename); + + if (count($fileParts) > 1 && $fileParts[0] == 'thumb') { + return $mediaManager->findOneBy(array( + 'id' => $fileParts[1], + 'context' => $context, + )) != null; + } + + return count($mediaManager->findBy(array( + 'providerReference' => $filename, + 'providers' => $this->getProviders(), + ))) > 0; + } +} diff --git a/Tests/Command/CleanMediaCommandTest.php b/Tests/Command/CleanMediaCommandTest.php new file mode 100755 index 000000000..b6fcacf30 --- /dev/null +++ b/Tests/Command/CleanMediaCommandTest.php @@ -0,0 +1,280 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\MediaBundle\Tests\Command; + +use Sonata\MediaBundle\Command\CleanMediaCommand; +use Sonata\MediaBundle\Filesystem\Local; +use Sonata\MediaBundle\Model\MediaManagerInterface; +use Sonata\MediaBundle\Provider\Pool; +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Filesystem\Tests\FilesystemTest as LegacyTestCase; +use Symfony\Component\Filesystem\Tests\FilesystemTestCase as BaseTestCase; + +// Polyfill for old symfony 2.3 TestCase class +if (class_exists('Symfony\Component\Filesystem\Tests\FilesystemTestCase')) { + class FilesystemTestCase extends BaseTestCase + { + } +} else { + class FilesystemTestCase extends LegacyTestCase + { + } +} + +class CleanMediaCommandTest extends FilesystemTestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|ContainerInterface + */ + protected $container; + + /** + * @var Application + */ + protected $application; + + /** + * @var ContainerAwareCommand + */ + protected $command; + + /** + * @var CommandTester + */ + protected $tester; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Pool + */ + private $pool; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|MediaManagerInterface + */ + private $mediaManager; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Local + */ + private $fileSystemLocal; + + /** + * {@inheritdoc} + */ + protected function setUp() + { + parent::setUp(); + + $this->container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + + $this->command = new CleanMediaCommand(); + $this->command->setContainer($this->container); + + $this->application = new Application(); + $this->application->add($this->command); + + $this->tester = new CommandTester($this->application->find('sonata:media:clean-uploads')); + + $this->pool = $pool = $this->getMockBuilder('Sonata\MediaBundle\Provider\Pool')->disableOriginalConstructor()->getMock(); + + $this->mediaManager = $mediaManager = $this->getMock('Sonata\MediaBundle\Model\MediaManagerInterface'); + + $this->fileSystemLocal = $fileSystemLocal = $this->getMockBuilder('Sonata\MediaBundle\Filesystem\Local')->disableOriginalConstructor()->getMock(); + $this->fileSystemLocal->expects($this->once())->method('getDirectory')->will($this->returnValue($this->workspace)); + + $this->container->expects($this->any()) + ->method('get') + ->will($this->returnCallback(function ($id) use ($pool, $mediaManager, $fileSystemLocal) { + switch ($id) { + case 'sonata.media.pool': + return $pool; + case 'sonata.media.manager.media': + return $mediaManager; + case 'sonata.media.adapter.filesystem.local': + return $fileSystemLocal; + } + + return; + })); + } + + public function testExecuteDirectoryNotExists() + { + $context = array( + 'providers' => array(), + 'formats' => array(), + 'download' => array(), + ); + + $this->pool->expects($this->once())->method('getContexts')->will($this->returnValue(array('foo' => $context))); + + $output = $this->tester->execute(array('command' => $this->command->getName())); + + $this->assertRegExp('@\'.+\' does not exist\s+done!@', $this->tester->getDisplay()); + + $this->assertSame(0, $output); + } + + public function testExecuteEmptyDirectory() + { + $this->filesystem->mkdir($this->workspace.DIRECTORY_SEPARATOR.'foo'); + + $context = array( + 'providers' => array(), + 'formats' => array(), + 'download' => array(), + ); + + $this->pool->expects($this->once())->method('getContexts')->will($this->returnValue(array('foo' => $context))); + + $output = $this->tester->execute(array('command' => $this->command->getName())); + + $this->assertRegExp('@Context: foo\s+done!@', $this->tester->getDisplay()); + + $this->assertSame(0, $output); + } + + public function testExecuteFilesExists() + { + $this->filesystem->mkdir($this->workspace.DIRECTORY_SEPARATOR.'foo'); + $this->filesystem->touch($this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'qwertz.ext'); + $this->filesystem->touch($this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'thumb_1_bar.ext'); + + $context = array( + 'providers' => array(), + 'formats' => array(), + 'download' => array(), + ); + + $provider = $this->getMockBuilder('Sonata\MediaBundle\Provider\FileProvider')->disableOriginalConstructor()->getMock(); + $provider->expects($this->any())->method('getName')->will($this->returnValue('fooprovider')); + + $this->pool->expects($this->any())->method('getContexts')->will($this->returnValue(array('foo' => $context))); + $this->pool->expects($this->any())->method('getProviders')->will($this->returnValue(array($provider))); + + $media = $this->getMock('Sonata\MediaBundle\Model\MediaInterface'); + + $this->mediaManager->expects($this->once())->method('findOneBy') + ->with($this->equalTo(array('id' => 1, 'context' => 'foo'))) + ->will($this->returnValue(array($media))); + $this->mediaManager->expects($this->once())->method('findBy') + ->with($this->equalTo(array('providerReference' => 'qwertz.ext', 'providers' => array('fooprovider')))) + ->will($this->returnValue(array($media))); + + $output = $this->tester->execute(array('command' => $this->command->getName())); + + $this->assertRegExp('@Context: foo\s+done!@', $this->tester->getDisplay()); + + $this->assertSame(0, $output); + } + + public function testExecuteFilesExistsVerbose() + { + $this->filesystem->mkdir($this->workspace.DIRECTORY_SEPARATOR.'foo'); + $this->filesystem->touch($this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'qwertz.ext'); + $this->filesystem->touch($this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'thumb_1_bar.ext'); + + $context = array( + 'providers' => array(), + 'formats' => array(), + 'download' => array(), + ); + + $provider = $this->getMockBuilder('Sonata\MediaBundle\Provider\FileProvider')->disableOriginalConstructor()->getMock(); + $provider->expects($this->any())->method('getName')->will($this->returnValue('fooprovider')); + + $this->pool->expects($this->any())->method('getContexts')->will($this->returnValue(array('foo' => $context))); + $this->pool->expects($this->any())->method('getProviders')->will($this->returnValue(array($provider))); + + $media = $this->getMock('Sonata\MediaBundle\Model\MediaInterface'); + + $this->mediaManager->expects($this->once())->method('findOneBy') + ->with($this->equalTo(array('id' => 1, 'context' => 'foo'))) + ->will($this->returnValue(array($media))); + $this->mediaManager->expects($this->once())->method('findBy') + ->with($this->equalTo(array('providerReference' => 'qwertz.ext', 'providers' => array('fooprovider')))) + ->will($this->returnValue(array($media))); + + $output = $this->tester->execute(array('command' => $this->command->getName(), '--verbose' => true)); + + $this->assertRegExp('@Context: foo\s+\'qwertz.ext\' found\s+\'thumb_1_bar.ext\' found\s+done!@', $this->tester->getDisplay()); + + $this->assertSame(0, $output); + } + + public function testExecuteDryRun() + { + $this->filesystem->mkdir($this->workspace.DIRECTORY_SEPARATOR.'foo'); + $this->filesystem->touch($this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'qwertz.ext'); + $this->filesystem->touch($this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'thumb_1_bar.ext'); + + $context = array( + 'providers' => array(), + 'formats' => array(), + 'download' => array(), + ); + + $provider = $this->getMockBuilder('Sonata\MediaBundle\Provider\FileProvider')->disableOriginalConstructor()->getMock(); + $provider->expects($this->any())->method('getName')->will($this->returnValue('fooprovider')); + + $this->pool->expects($this->any())->method('getContexts')->will($this->returnValue(array('foo' => $context))); + $this->pool->expects($this->any())->method('getProviders')->will($this->returnValue(array($provider))); + + $this->mediaManager->expects($this->once())->method('findOneBy') + ->with($this->equalTo(array('id' => 1, 'context' => 'foo'))) + ->will($this->returnValue(array())); + $this->mediaManager->expects($this->once())->method('findBy') + ->with($this->equalTo(array('providerReference' => 'qwertz.ext', 'providers' => array('fooprovider')))) + ->will($this->returnValue(array())); + + $output = $this->tester->execute(array('command' => $this->command->getName(), '--dry-run' => true)); + + $this->assertRegExp('@Context: foo\s+\'qwertz.ext\' is orphanend\s+\'thumb_1_bar.ext\' is orphanend\s+done!@', $this->tester->getDisplay()); + + $this->assertSame(0, $output); + } + + public function testExecute() + { + $this->filesystem->mkdir($this->workspace.DIRECTORY_SEPARATOR.'foo'); + $this->filesystem->touch($this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'qwertz.ext'); + $this->filesystem->touch($this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'thumb_1_bar.ext'); + + $context = array( + 'providers' => array(), + 'formats' => array(), + 'download' => array(), + ); + + $provider = $this->getMockBuilder('Sonata\MediaBundle\Provider\FileProvider')->disableOriginalConstructor()->getMock(); + $provider->expects($this->any())->method('getName')->will($this->returnValue('fooprovider')); + + $this->pool->expects($this->any())->method('getContexts')->will($this->returnValue(array('foo' => $context))); + $this->pool->expects($this->any())->method('getProviders')->will($this->returnValue(array($provider))); + + $this->mediaManager->expects($this->once())->method('findOneBy') + ->with($this->equalTo(array('id' => 1, 'context' => 'foo'))) + ->will($this->returnValue(array())); + $this->mediaManager->expects($this->once())->method('findBy') + ->with($this->equalTo(array('providerReference' => 'qwertz.ext', 'providers' => array('fooprovider')))) + ->will($this->returnValue(array())); + + $output = $this->tester->execute(array('command' => $this->command->getName())); + + $this->assertRegExp('@Context: foo\s+\'qwertz.ext\' was successfully removed\s+\'thumb_1_bar.ext\' was successfully removed\s+done!@', $this->tester->getDisplay()); + + $this->assertSame(0, $output); + } +}