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);
+ }
+}