Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Remove Magento's orphan images web console</title>
<link href='https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300,700' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Inconsolata:400,700&subset=latin,latin-ext' rel='stylesheet'
type='text/css'>
<style type="text/css">
body {
font-family: "Open Sans", Arial, sans-serif;
margin-left: 25px;
margin-right: 30px;
padding: 0 0 0 0;
}
pre {
font-family: Inconsolata, "Courier New", monospace;
font-size: 15px;
clear: both;
margin: 0 -0px 0 0;
background: #000;
border: 1px groove #ccc;
color: #ccc;
display: block;
width: 100%;
min-height: 600px;
padding: 5px 5px 5px 5px;
}
.logo {
float: left;
margin-right: 25px;;
}
.scriptInfo {
float: left;
}
h1 {
font-size: 16px;
margin-top: 0;
line-height: 49px;
margin-bottom: 0;
}
h1 span {
font-family: Inconsolata, "Courier New", monospace;
}
.header {
width: 100%;
height: auto;
display: table;
margin-top: 25px;
margin-bottom: 25px;;
}
</style>
</head>
<body>
<div class="header">
<div class="logo">
<img src="skin/frontend/default/modern/images/logo.gif" alt="Magento's Logo" width="173" height="49">
</div>
<div class="scriptInfo">
<h1>Delete Catalog Orphan Images <span>1.1</span></h1>
</div>
</div>
<pre>
<?php
require 'app/Mage.php';
/**
* NOTE: Set this to false if you want to actually delete files.
*/
$dryRun = true;
$job = new OrphanImagesCleaner();
$job->run($dryRun);
/**
* Class OrphanImagesCleaner
*
* @author Jeroen Boersma <jeroen@srcode.nl>
* @author Adriano Cataluddi <acataluddi@gmail.com>
*/
class OrphanImagesCleaner
{
/**
* @var bool
*/
protected $dryRun = true;
/**
* Executes the tools
* @param bool $dryRun
*/
public function run($dryRun = true)
{
$this->dryRun = $dryRun;
$duplicateCount = 0;
$duplicateTotalSize = 0;
$orphansCount = 0;
$orphansTotalSize = 0;
if (!Mage::isInstalled()) {
$this->wl('Application is not installed yet, please complete install wizard first.');
exit;
}
set_time_limit(0);
session_write_close();
umask(0);
Mage::app('admin')->setUseSessionInUrl(false);
$mediaPath = $this->getRootPath() . '/media/catalog/product';
if ($this->dryRun)
$this->wl('DRY RUN mode: the script will NOT modify any file or record.');
$this->wl();
$this->wl(' Magento release: ' . Mage::getVersion());
$this->wl(' Media path: ' . $mediaPath);
$this->wl();
$this->wl();
$this->wl('[Phase 1/2] Looking for duplicate products images...');
$this->wl(str_repeat('-', 80));
$connection = Mage::getSingleton('core/resource')
->getConnection('core_write');
$sql = 'select distinct '
. 'cp.entity_id, '
. 'cpg.value_id, '
. 'cpv.value as default_value, '
. 'cpg.value '
. 'from catalog_product_entity as cp '
. 'join catalog_product_entity_varchar as cpv on cp.entity_id = cpv.entity_id '
. 'join catalog_product_entity_media_gallery as cpg on cp.entity_id = cpg.entity_id '
. 'WHERE '
. 'cpv.attribute_id in(select attribute_id from eav_attribute where frontend_input = \'media_image\') '
. 'and '
. 'cpv.value != cpg.value;';
$results = $connection->fetchAll($sql);
$this->wl(sprintf('Found %s items to process.', sizeof($results)), true);
$lastEntityId = null;
$origSums = array();
foreach ($results as $row) {
if ($row['entity_id'] != $lastEntityId) {
$lastEntityId = $row['entity_id'];
$origSums = array();
}
$origFile = $mediaPath . $row['default_value'];
if (!file_exists($origFile)) {
continue;
}
$file = $mediaPath . $row['value'];
if (file_exists($file)) {
if (!isset($origSums[$origFile])) {
$origSums[$origFile] = md5_file($origFile);
}
$sum = md5_file($file);
if (!in_array($sum, $origSums)) {
$origSums[$file] = $sum;
}
else {
$this->wl(sprintf('Deleting image "%s" (#%s)', $file, $row['entity_id']), true);
$duplicateCount++;
$duplicateTotalSize += filesize($file);
if (!$this->dryRun) unlink($file);
}
}
if (!file_exists($file)) {
$this->wl(sprintf('Deleting record for "%s" (#%s)', $file, $row['entity_id']), true);
$deleteSql = 'delete from catalog_product_entity_media_gallery where value_id = ' . $row['value_id'] . ';';
if (!$this->dryRun) $connection->query($deleteSql);
}
}
// Find files on filesystem which aren't listed in the database
$this->wl();
$this->wl('[Phase 1/2] Finding files on filesystem which aren\'t listed in the database...');
$this->wl(str_repeat('-', 80));
$files = glob($mediaPath . '/[A-z0-9]/*/*');
foreach ($files as $file) {
$searchFile = str_replace($mediaPath, '', $file);
// Lookup
$mediaSql = "select count(*) as records from catalog_product_entity_media_gallery where value = '{$searchFile}'";
$mediaCount = $connection->fetchOne($mediaSql);
if ($mediaCount < 1) {
$orphansCount++;
$orphansTotalSize += filesize($file);
$this->wl(sprintf('Deleting image "%s"', $file), true);
if (!$this->dryRun) unlink($file);
}
}
$this->wl();
$this->wl('Done.');
$this->wl(str_repeat('-', 80));
$this->wl(sprintf(' Total duplicate images: %s (%s)', $duplicateCount, $this->formatBytes($duplicateTotalSize)));
$this->wl(sprintf(' Total orphan images: %s (%s)', $orphansCount, $this->formatBytes($orphansTotalSize)));
$this->wl(str_repeat('-', 80));
}
/**
* @param boolean $dryRun
*/
public function setDryRunEnabled($dryRun)
{
$this->dryRun = $dryRun;
}
/**
* @return boolean
*/
public function isDryRunEnabled()
{
return $this->dryRun;
}
/**
* Writes a line in console.
* @param $line
* @param bool $notifyDryRun
*/
protected function wl($line = null, $notifyDryRun = false)
{
($notifyDryRun && $this->dryRun && ($line !== null)) ?
$dryLabel = 'DRY RUN | ' :
$dryLabel = '';
print $dryLabel . $line . "\n";
}
/**
* Returns the script root path
* @return string
*/
protected function getRootPath()
{
return (dirname(__FILE__));
}
/**
* Format bytes
* @author MrCaspan (https://github.com/MrCaspan)
* @param $bytes
* @return string
*/
protected function formatBytes($bytes)
{
$i = floor(log($bytes, 1024));
return round($bytes / pow(1024, $i), [0, 0, 2, 2, 3][$i]) . ['B', 'kB', 'MB', 'GB', 'TB'][$i];
}
}
?>
</pre>
</body>
</html>
@acataluddi

This comment has been minimized.

Copy link
Owner Author

@acataluddi acataluddi commented May 23, 2016

Remove Magento duplicate and orphan products images

This PHP script removes both duplicate and orphan images in Magento (tested with 1.9.2.1).
Putting the script on Magento root path, you can call it by browser and read messages in the web console like in the following screenshot.

removeorphanimages php

The script is an improvement of Jeroen Boersma's original Gist (that I would like to thank for the first release) with addition of Dry-Run mode and web console.

@dvakerlis

This comment has been minimized.

Copy link

@dvakerlis dvakerlis commented Jun 22, 2016

This script it show me all the unused images...correct? and how i will make this script to delete this images?

@adrian-green

This comment has been minimized.

Copy link

@adrian-green adrian-green commented Oct 6, 2016

Just want to point out that there can be more than just 3 media gallery attributes. You should not hard-code them. Instead create a subquery to fetch them into a list. Example:

cpv.attribute_id IN ( SELECT attribute_id FROM eav_attribute WHERE frontend_input = 'media_image' )

@acataluddi

This comment has been minimized.

Copy link
Owner Author

@acataluddi acataluddi commented Jul 5, 2017

Just want to point out that there can be more than just 3 media gallery attributes. You should not hard-code them. Instead create a subquery to fetch them into a list. Example:

cpv.attribute_id IN ( SELECT attribute_id FROM eav_attribute WHERE frontend_input = 'media_image' )

Thank you @adrian-green, for some reason I didn't receive your comment notification. Thank you for the code improvement, I've integrated your good suggestion.

Regards,
Adriano

@acataluddi

This comment has been minimized.

Copy link
Owner Author

@acataluddi acataluddi commented Jul 5, 2017

This script it show me all the unused images...correct? and how i will make this script to delete this images?

@dvakerlis, the script will also actually remove the unused images. Look please around line 208:

if ($mediaCount < 1) {
    $orphansCount++;
    $this->wl(sprintf('Deleting image "%s"', $file), true);
    if (!$this->dryRun) unlink($file);
}

Regards,
Adriano

@chris-ath

This comment has been minimized.

Copy link

@chris-ath chris-ath commented Sep 17, 2017

How can i run the script to non dry mode?
I was try this without success:
protected $dryRun = false;

Regards,
Chris

@anodi

This comment has been minimized.

Copy link

@anodi anodi commented Sep 20, 2017

I am also wondering how to use this script so that it actually deletes files. Thanks!!

BR, Mikko

@zpvini

This comment has been minimized.

Copy link

@zpvini zpvini commented Oct 6, 2017

@anodi @chris-ath
try changing line 79 for:
$job = new OrphanImagesCleaner(false);

@webdreamsnc

This comment has been minimized.

Copy link

@webdreamsnc webdreamsnc commented Apr 13, 2018

@acataluddi
Hi, we improved your script to get table's name from magento core (including tables prefix options).
Thi is run function edited:

public function run()
{
$duplicateCount = 0;
$orphansCount = 0;
if (!Mage::isInstalled()) {
$this->wl('Application is not installed yet, please complete install wizard first.');
exit;
}
set_time_limit(0);
session_write_close();
umask(0);
Mage::app('admin')->setUseSessionInUrl(false);
$mediaPath = $this->getRootPath() . '/media/catalog/product';
if ($this->dryRun)
$this->wl('DRY RUN mode: the script will NOT modify any file or record.');
$this->wl();
$this->wl(' Magento release: ' . Mage::getVersion());
$this->wl(' Media path: ' . $mediaPath);
$this->wl();
$this->wl();
$this->wl('[Phase 1/2] Looking for duplicate products images...');
$this->wl(str_repeat('-', 80));
$connection = Mage::getSingleton('core/resource')
->getConnection('core_write');
$sql = 'select distinct '
. 'cp.entity_id, '
. 'cpg.value_id, '
. 'cpv.value as default_value, '
. 'cpg.value '
. 'from '.Mage::getSingleton('core/resource')->getTableName('catalog_product_entity').' as cp '
. 'join '.Mage::getSingleton('core/resource')->getTableName('catalog_product_entity_varchar').' as cpv on cp.entity_id = cpv.entity_id '
. 'join '.Mage::getSingleton('core/resource')->getTableName('catalog_product_entity_media_gallery').' as cpg on cp.entity_id = cpg.entity_id '
. 'WHERE '
. 'cpv.attribute_id in(select attribute_id from '.Mage::getSingleton('core/resource')->getTableName('eav_attribute').' where frontend_input = 'media_image') '
. 'and '
. 'cpv.value != cpg.value;';
$results = $connection->fetchAll($sql);
$this->wl(sprintf('Found %s items to process.', sizeof($results)), true);
$lastEntityId = null;
$origSums = array();
foreach ($results as $row) {
if ($row['entity_id'] != $lastEntityId) {
$lastEntityId = $row['entity_id'];
$origSums = array();
}
$origFile = $mediaPath . $row['default_value'];
if (!file_exists($origFile)) {
continue;
}
$file = $mediaPath . $row['value'];
if (file_exists($file)) {
if (!isset($origSums[$origFile])) {
$origSums[$origFile] = md5_file($origFile);
}
$sum = md5_file($file);
if (!in_array($sum, $origSums)) {
$origSums[$file] = $sum;
}
else {
$this->wl(sprintf('Deleting image "$s" (#%s)', $file, $row['entity_id']), true);
$duplicateCount++;
if (!$this->dryRun) unlink($file);
}
}
if (!file_exists($file)) {
$this->wl(sprintf('Deleting record for "%s" (#%s)', $file, $row['entity_id']), true);
$deleteSql = 'delete from '.Mage::getSingleton('core/resource')->getTableName('catalog_product_entity_media_gallery').' where value_id = ' . $row['value_id'] . ';';
if (!$this->dryRun) $connection->query($deleteSql);
}
}
// Find files on filesystem which aren't listed in the database
$this->wl();
$this->wl('[Phase 1/2] Finding files on filesystem which aren't listed in the database...');
$this->wl(str_repeat('-', 80));
$files = glob($mediaPath . '/[A-z0-9]//');
foreach ($files as $file) {
$searchFile = str_replace($mediaPath, '', $file);
// Lookup
$mediaSql = "select count(*) as records from ".Mage::getSingleton('core/resource')->getTableName('catalog_product_entity_media_gallery')." where value = '{$searchFile}'";
$mediaCount = $connection->fetchOne($mediaSql);
if ($mediaCount < 1) {
$orphansCount++;
$this->wl(sprintf('Deleting image "%s"', $file), true);
if (!$this->dryRun) unlink($file);
}
}
$this->wl();
$this->wl('Done.');
$this->wl(str_repeat('-', 80));
$this->wl(sprintf(' Total duplicate images: %s', $duplicateCount));
$this->wl(sprintf(' Total orphan images: %s', $orphansCount));
$this->wl(str_repeat('-', 80));
}

@Dogleg

This comment has been minimized.

Copy link

@Dogleg Dogleg commented May 11, 2018

Fix above worked for me but there is a single vs double quote mark issue in the db query string; needs to be:

. 'cpv.attribute_id in(select attribute_id from '.Mage::getSingleton('core/resource')->getTableName('eav_attribute').' where frontend_input = \'media_image\') '

…the single quotes around 'media_image' need to be slashed out.

@LiamKarlMitchell

This comment has been minimized.

Copy link

@LiamKarlMitchell LiamKarlMitchell commented Aug 29, 2018

Thanks for this, It would be neat to have an option to get a total disk space in-use by orphaned images when doing the dry-run.
http://php.net/manual/en/function.filesize.php

@PeterBrain

This comment has been minimized.

Copy link

@PeterBrain PeterBrain commented Jan 4, 2019

There is a little typo at line 179 ($ instead of %):

change
$this->wl(sprintf('Deleting image "$s" (#%s)', $file, $row['entity_id']), true);
to
$this->wl(sprintf('Deleting image "%s" (#%s)', $file, $row['entity_id']), true);

@Ang90

This comment has been minimized.

Copy link

@Ang90 Ang90 commented Apr 14, 2020

Beautiful! If I understand correctly, this script will delete the images that are no longer linked to no product ... That is, if I have only "disabled" products, in this case the images will not be deleted? It is important to keep them for me ... Also how do I run the script? Load in the foot and what do I type in the URL? Thank you

@acataluddi

This comment has been minimized.

Copy link
Owner Author

@acataluddi acataluddi commented Apr 16, 2020

There is a little typo at line 179 ($ instead of %):

Thanks @PeterBrain, fixed.

@acataluddi

This comment has been minimized.

Copy link
Owner Author

@acataluddi acataluddi commented Apr 16, 2020

hat is, if I have only "disabled" products, in this case the images will not be deleted?

Thanks @Ang90, much appreciated. Yes, correct. It should only remove the orphan images (not bound to any product anymore).
Anyway, as a general suggestion, I presume you have a test enviroment to run the script before: it's always the right think to do.

Thanks,
Adriano

@acataluddi

This comment has been minimized.

Copy link
Owner Author

@acataluddi acataluddi commented Apr 16, 2020

Thanks for this, It would be neat to have an option to get a total disk space in-use by orphaned images when doing the dry-run.

Hi @LiamKarlMitchell,
I've added the suggested feature.

Regards,
Adriano

@Ang90

This comment has been minimized.

Copy link

@Ang90 Ang90 commented Apr 16, 2020

Thanks a lot! But how do I run the script? I need to possibly run it via the URL browser, otherwise from ssh what are the exact commands? Thank you

@acataluddi

This comment has been minimized.

Copy link
Owner Author

@acataluddi acataluddi commented Apr 16, 2020

Hi @Ang90,

just upload it to your Magento root path and call the URL https://www.your-magento-store.com/removeOrphanImages.php.

Please Note
By default, it runs in "dry-mode" (does not actually deletes files). To disable the dry mode you need to change the following line from:

/**
 * NOTE: Set this to false if you want to actually delete files.
 */
$dryRun = true;

to:

/**
 * NOTE: Set this to false if you want to actually delete files.
 */
$dryRun = false;

Regards,
Adriano

@Ang90

This comment has been minimized.

Copy link

@Ang90 Ang90 commented Apr 16, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.