[Gelöst] Media DB aufräumen (auf Filesystem nicht mehr referenzierte Media Dateien löschen)

Hallo zusammen. Ich habe versucht die Media DB aufuräumen indem ich die beiden Commands „sw:media:cleanup --delete“ und „sw:thumbnail:cleanup --delete“ in der console ausgeführt habe. Die Commands haben einige Einträge bereinigt. Trotzdem scheinen auf dem Filesystem viel mehr Dateien (ca. 50’000) noch übrig zu sein als in der Media DB Verwaltung im Backend aufgelistet (ca. 12’000). (Der Papierkorb der Media DB ist leer). In der DB Tabelle s_media habe ich nachgeschaut und dort sind auch nur ca. die 12’000 drin.

Die Frage ist, gibt es ein Script oder Command welches auf dem Filesystem nicht mehr referenzierte Media Dateien löscht?

Danke schon vorab

Gruss

Mark

Hallo Mark,

das Filesystem hat immer Faktor x mehr. Es werden ja je Datei auch diverse Thumbnails/Größen erzeugt

Sebastian

Ja schon aber bestimmt nicht soviele im Thumbnails sind es ca. ebenfalls 12’000.

Nachdem ich mir den Source code der beiden Commands „ThumbnailGenerateCommand“ und „ThumbnailCleanupCommand“ angeschaut habe denke ich habe ich ein Bug von SW gefunden. Bei mir sind haufenweise Thumbnails mit der Endung „'xxxxx @2x.jpg“ vohanden die durch den „Shopware\Components\Thumbnail\Manager.createMediaThumbnail()“ generiert werden, aber später unter Umständen von „sw:thumbnail:cleanup“ nicht mehr gelöscht werden. Somit bleiben Files dann auf dem Filesystem liegen

.z.B. Thumbnails mit Endung „@2x.jpg“ im „media“ Folder

\shop\media\image\17\47\3e\lg_fjdhygjb_1_600x600@2x.jpg
\shop\media\image\17\4f\f1\lg_5_156ddf1314603b_200x200@2x.jpg
\shop\media\image\17\5c\c3\lg_fngj0kkj_1_140x140@2x.jpg

 Hier der code Snippet von „Manager.createMediaThumbnail()“ welche die Thumbnail mit dieser Endung anlegt. Ich nehme an es handelt sich bei „2x“ um High Resolution Thumbnails.

        foreach ($parameters['sizes'] as $size) {
            $suffix = $size['width'] . 'x' . $size['height'] . '@2x';

            $destinations = $this->getDestination($media, $suffix);
            foreach ($destinations as $destination) {
                $this->generator->createThumbnail(
                    $parameters['path'],
                    $destination,
                    $size['width']*2,
                    $size['height']*2,
                    $parameters['keepProportions'],
                    $highDpiQuality
                );
            }
        }

Und hier das Gegenstück vom Cleanup command „ThumbnailCleanupCommand.getMediaThumbnailPaths()“ welches die Pfade der Dateien zurückliefert welche gelöscht werden sollen. Da die Thumbnails nicht in der DB referenziert sind, muss der Name auf dem Filesystem zusammengebaut werden. Leider scheint da die Variante der High Resolution Thumbnails mit „@2x“ Endung nicht berücksichtigt zu werden.

       //iterate thumbnail sizes
        foreach ($sizes as $size) {
            if (strpos($size, 'x') === false) {
                $size = $size . 'x' . $size;
            }

            $thumbnailDir = Shopware()->DocPath('media_' . strtolower($media['type'])) . 'thumbnail' . DIRECTORY_SEPARATOR;
            $path = $thumbnailDir . $this->removeSpecialCharacters($media['name']) . '_' . $size;
            if (DIRECTORY_SEPARATOR !== '/') {
                $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
            }

            $thumbnails[] = $path . '.jpg';

            if ($media['extension'] !== 'jpg') {
                $thumbnails[] = $path . '.' . $media['extension'];
            }
        }

Wenn das jemand Bestätigen kann werde ich gerne ein „Issue“ (Bug) im SW Issue Tracker eröffnen.

Ich habe mittlerweile ein Plugin geschrieben, der den Abgleich mit der Media DB ( inkl. mit den Thumnbails) macht und die dann nicht mehr referenzierte Bilder auf dem Filesystem löscht. Das machen leider die beiden Commands “sw:media:cleanup” und “sw:thumbnail:cleanup” eben nicht so bzw. nicht ganz korrekt. Schreibt mir eine Message falls Ihr dasselbe Problem habt und Interesse besteht.

Da sich viel per Message bei mir gemeldet habe, kann ich hier ein einfaches CLI Plugin bzw. das wesentliche Skript davon zur Verfügung stellen. Wenn es geholfen habt, bitte verifiziert, ob Euer Problem auch aufgrund dieser High Resolution Thumbnails (mit “@2x.jpg” Endung) entstanden ist.  Wenn ja sollten wir ein Bug bei Shopwaresupport eröffnen.

Das ganze Plugin hier sollte helfen diese unrefenzierten High Resolution Thumbnails zu identifizieren und löschen. Wem es geholfen hat kann sich gerne bei mir bedanken oder eine Paypal donation hinterlassen.

Benützung - Achtung, auf eigene Gefahr !

  1. Löscht zuerst die unrefenzierten Media Dateien über die bereits vorhandenen Möglichkeiten im Backend, entweder über das Menü “Inhalte > Medienverwaltung > Papierkorb > löschen” oder besser über die Console auf der Kommandozeilenumgebung “php console sw:media:cleanup” und “php console sw:thumbnail:cleanup”
  2. prüft ob der Schritt 1 Euer Problem schon gelöst hat z.B. genügend Speicherplatz wieder frei wurde
  3. Macht ein Backup der DB und der Media Dateien, checkt wieviel Speicherplatz momentan in Benützung ist. Prüft ob verhältnismässig zuviele High Resolution Plugins immer noch auf dem Filesystem besteht (mit “@2x.jpg” Endung)
  4. Installiert das Plugin
  5. ruft über die console den neuen Befehl auf “php console megloff:media:cleanup”   – dies löscht die unrefenzierten HD Thumbnails, beachtet das dies lange dauern könnte und je nach der Menge Einträge in der DB viel Speicher benötigt. Eventuell muss dabei die PHP.ini kurzfristig angepasst werden

 

Datei “Shopware/Plugins/Local/Core/ExtendedConsole/Bootstrap.php”

 

class Shopware_Plugins_Core_ExtendedConsole_Bootstrap extends Shopware_Components_Plugin_Bootstrap
{
	
    /**
     * @return bool
     */
    public function install()
    {
        $this->subscribeEvents();

        return true;
    }

    /**
     * Registers all necessary events and hooks.
     */
    private function subscribeEvents()
    {  	 
    	$this->subscribeEvent(
    			'Shopware_Console_Add_Command',
    			'onAddConsoleCommand'
    			);
    }

    /**
     * @param Enlight_Event_EventArgs $args
     */
    public function onEnlightControllerFrontStartDispatch(Enlight_Event_EventArgs $args)
    { }
    
    // Just register one single command
    public function onAddConsoleCommand(Enlight_Event_EventArgs $args)
    {
    	require_once __DIR__. '/Commands/MediaCleanupCommand.php';
    	return new \Shopware\Commands\MediaCleanupCommand();
    }

}

 

Datei “Shopware/Plugins/Local/Core/ExtendedConsole/Commands/MediaCleanupCommand.php”

namespace Shopware\Commands;

use Shopware\Components\Model\ModelManager;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;

class MediaCleanupCommand extends ShopwareCommand
{
	/**
	 * @var \Shopware\Bundle\MediaBundle\MediaService $mediaService
	 */
	private $mediaService;
	/**
	 * {@inheritdoc}
	 */
	protected function configure()
	{
		$this
		->setName('megloff:media:cleanup')
		->setDescription('cleans up unused media files from filesystem')
		->addOption('delete', false, InputOption::VALUE_NONE, "Delete unused media files.");
		
		$this->mediaService = Shopware()->Container()->get('shopware_media.media_service');
	}
	
	/**
	 * @return \Shopware\Bundle\MediaBundle\MediaService
	 */
	protected function getMediaservice() {
		return $this->mediaService;
	}

	/**
	 * {@inheritdoc}
	 */
	protected function execute(InputInterface $input, OutputInterface $output)
	{
	 
		 $output->writeln('');
		 $albums = $this->getAlbums();
		 $medias = $this->getRegisteredMediaPaths($albums);
		 $output->writeln('number of registered media in DB found: ' . count($medias));
		 $missing_medias = array();
		 foreach ($medias as $media) {
		 	if(!$this->getMediaservice()->has($media)) {
		 		$missing_medias[] = $media;
		 	}
		 }
		 if(count($missing_medias)) {
		 	$output->writeln('number of missing media files in DB found: ' . count($missing_medias));
		 }
		 
		 $thumbnails = $this->getThumbnailPaths($albums);
		 $output->writeln('number of registered thumbnails files found: ' . count($thumbnails));
		 $files = $this->listFiles("media");
		 $output->writeln('number of files in media folder found: ' . count($files));
		 $files = array_diff($files,$medias,$thumbnails);
		 $output->writeln('number of unused files: ' . count($files));
		 if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
		 	foreach($files as $file) {
		 		$output->writeln("found unused file: " .$file);
		 	}
		 }
		 if (count($files)) {	
			 if ($input->getOption('delete')) {
			 	if ($input->isInteractive()) {
			 		$dialog = $this->getHelper('dialog');
			 		if (!$dialog->askConfirmation($output, 'Are you sure you want to delete these files? [y/N] ', false)) {
			 			return;
			 		}
			 	}
			 
	 			$fs = new Filesystem();
	 			$baseDir = Shopware()->DocPath();
			 				
			 	foreach($files as $file) {
			 	 	$fs->remove($file);
			 		$output->writeln("deleted file " .$file);
			 	}
			 }
		 }
		
		$output->writeln('');
	}
	
	
	private function getAlbums() {
		/**
		 * @var \Shopware\Components\Model\ModelManager $em
		 */
		$em = $this->container->get('models');
		
		$builder = $em->createQueryBuilder();
		$builder->select(array('album', 'settings', 'media'))
		->from('Shopware\Models\Media\Album', 'album')
		->leftJoin('album.settings', 'settings')
		->leftJoin('album.media', 'media');

		$result = $builder->getQuery()->getResult(\Doctrine\ORM\AbstractQuery::HYDRATE_ARRAY);
		
		return $result;
	}
	
	/**
	 * Returns all thumbnails paths
	 * @return array
	 */	
	private function getRegisteredMediaPaths($albums) {
						
		$result = array();		
		foreach ($albums as $album) {
			// $output->writeln("Processing album {$album['name']} (ID: {$album['id']})");
			foreach ($album['media'] as $media) {
				$path = Shopware()->oldPath() . $media['path'];
				$result[] = $this->getMediaService()->encode($path);
			}
		}
		return $result;
	}
	
	private function getThumbnailPaths($albums) {
		$result = array();
		foreach ($albums as $album) {
			$sizes = $album['settings']['thumbnailSize'];
			if(!empty($sizes)) {
				foreach ($album['media'] as $media) {
					$paths = $this->getMediaThumbnailPaths($media, explode(';', $sizes));
					foreach ($paths as $path) {
						if ($this->getMediaService()->has($path)) {
							$path = $this->getMediaService()->encode($path);

						}
					}
				}
			}
		}	
		return $result;
	}
	
	
	/**
	 * Returns all thumbnails paths according to the given media object
	 *
	 * @param $media
	 * @param $sizes
	 * @return array
	 */
	private function getMediaThumbnailPaths($media, $sizes)
	{
		$sizes = array_merge($sizes, array('140x140'));
		$sizes = array_unique($sizes);
	
		$thumbnails = array();
	
		//iterate thumbnail sizes
		foreach ($sizes as $size) {
			if (strpos($size, 'x') === false) {
				$size = $size . 'x' . $size;
			}
	
			$thumbnailDir = Shopware()->DocPath('media_' . strtolower($media['type'])) . 'thumbnail' . DIRECTORY_SEPARATOR;
			$path = $thumbnailDir . $this->removeSpecialCharacters($media['name']) . '_' . $size;
			if (DIRECTORY_SEPARATOR !== '/') {
				$path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
			}
	
			$thumbnails[] = $path . '.jpg';
			$thumbnails[] = $path . '@2x.jpg';
				
			if ($media['extension'] !== 'jpg') {
				$thumbnails[] = $path . '.' . $media['extension'];
				$thumbnails[] = $path . '@2x.' . $media['extension'];
			}
		}
	
		return $thumbnails;
	}
	
	/**
	 * Removes special characters from a filename
	 *
	 * @param $name
	 * @return string
	 */
	private function removeSpecialCharacters($name)
	{
		$name = iconv('utf-8', 'ascii//translit', $name);
		$name = preg_replace('#[^A-z0-9\-_]#', '-', $name);
		$name = preg_replace('#-{2,}#', '-', $name);
		$name = trim($name, '-');
		return mb_substr($name, 0, 180);
	}
	
	
	private function listFiles($dir, &$results = array()){
	    $files = scandir($dir);
	    foreach($files as $key => $value){
	    	if ($value == ".htaccess") continue;
	    	if ($value == ".gitkeep") continue;
	        $path = $dir."/".$value;
	        if(!is_dir($path)) {
	        	$results[] = $path;
	        } else if($value != "." && $value != "..") {
	            $this->listFiles($path, $results);
	        }
	    }
	
	    return $results;
	}
}

 

7 „Gefällt mir“

(Y)

Ciao zusammen, 

der Artikel ist zwar schon älter, aber ich habe auch irgendwie das Problem mit extrem vielen komischen Dateien (103.000 Dateien) im media-Ordner. 
Ich kam von Version 4.1.3 und habe über 4.2.0/4.2.3/4.3.7 usw. das System nun auf 5.1.6
Ein weiteres Update von 5.1.6 auf 5.2 schlug wegen der vielen Dateien fehl (Hoster hat eine Grenze bei 260.000 Dateien und ich habe dort noch andere Seiten)… nun ist die DB etwas komisch, da die Backups nicht auf leere DBs gespielt worden sind. (aber das mache ich dann manuell) 

Ich habe ca. 4000 Bilder, mit je 6 Thumbnails
- angeblich soll ja für normale Bilder 200x200, 600x600 und 1280x1280 reichen

  • und nut für Banner die anderen Grössen  800×800, 1280×1280 und 1920×1920 notwendig sein… Ging bei mir nicht. Daher haben alle Bilder der 20 Unterordner vom Baum Artikel nun jeweils 6 Thumbnails. 

Aber das würde doch maximal (6 + 1) * 4000 = 28.000 Bilder ergeben, oder? 
 

Leider kann ich das plugin nicht “bauen”, wohl zu doof… 

Wie muss die Struktur aussehen, damit ich sie mit dem Plugin Manager nutzen kann? 
Oder hat jemand das ZIP-File?

 

Megloff_Media_cleanup

├── Components

│   └── Bootstrap.php

├── MediaCleanupCommand.php

└── Resources

    ├── config.xml

    └── services.xml

Merci vielmal.