├── README.md ├── composer.json └── src ├── Console └── RemoveDuplicate.php ├── etc ├── di.xml └── module.xml └── registration.php /README.md: -------------------------------------------------------------------------------- 1 | # Elgentos - Remove Duplicate Product images in Magento 2 2 | 3 | This Extension allows you to find duplicate product images from your product list and from this list you can easily remove them by running a command. 4 | 5 | ## Installation 6 | 7 | 1) Go to your Magento root folder 8 | 2) Download the extension using composer: 9 | ``` 10 | composer require elgentos/magento2-product-duplicate-images-remove 11 | ``` 12 | 3) Run setup commands: 13 | 14 | ``` 15 | php bin/magento setup:upgrade 16 | ``` 17 | 18 | 4) Run the command: 19 | 20 | ``` 21 | php bin/magento duplicate:remove 22 | ``` 23 | Eg: 24 | ```sh 25 | # Use unlink 26 | php bin/magento duplicate:remove -u1 27 | 28 | # Turn off dry run 29 | php bin/magento duplicate:remove -d0 30 | 31 | # Combine unlink and turn off dry run 32 | php bin/magento duplicate:remove -u1 -d0 33 | 34 | # Specific on some sku 35 | php bin/magento duplicate:remove -u1 -d0 SKU1 SKU2 SKU3 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elgentos/magento2-product-duplicate-images-remove", 3 | "description":"Magento 2 find duplicate product images from your product list and from this list you can easily remove them by running a command", 4 | "keywords": [ 5 | "magento 2", 6 | "magento", 7 | "m2", 8 | "duplicate product images", 9 | "duplicate product images remove", 10 | "product images remove", 11 | "magento 2 extension", 12 | "magento 2 extension free" 13 | ], 14 | "require": { 15 | "magento/framework": "^103.0" 16 | }, 17 | "type": "magento2-module", 18 | "license": [ 19 | "OSL-3.0", 20 | "AFL-3.0" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "Peter Jaap Blaakmeer", 25 | "email": "peterjaap@elgentos.nl" 26 | } 27 | ], 28 | "autoload": { 29 | "files": [ 30 | "src/registration.php" 31 | ], 32 | "psr-4": { 33 | "Elgentos\\RemoveDuplicateImage\\": "src/" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Console/RemoveDuplicate.php: -------------------------------------------------------------------------------- 1 | searchCriteriaBuilder = $searchCriteriaBuilder; 80 | $this->state = $state; 81 | $this->productRepository = $productRepository; 82 | $this->storeManager = $storeManager; 83 | $this->directoryList = $directoryList; 84 | $this->resource = $resource; 85 | $this->fileDriver = $fileDriver; 86 | parent::__construct(); 87 | } 88 | 89 | /** 90 | * @inheritdoc 91 | */ 92 | protected function configure() 93 | { 94 | $this->setName('duplicate:remove') 95 | ->setDescription('Remove duplicate product images') 96 | ->addOption( 97 | 'unlink', 98 | 'u', 99 | InputOption::VALUE_OPTIONAL, 100 | 'Unlink the duplicate files from system', 101 | false 102 | ) 103 | ->addOption( 104 | 'dryrun', 105 | 'd', 106 | InputOption::VALUE_OPTIONAL, 107 | 'Dry-run does not delete any values or files', 108 | true 109 | ) 110 | ->addArgument( 111 | 'products', 112 | InputArgument::IS_ARRAY, 113 | 'Product entity SKUs to filter on', 114 | null 115 | ); 116 | } 117 | 118 | /** 119 | * @inheritdoc 120 | */ 121 | protected function execute(InputInterface $input, OutputInterface $output) 122 | { 123 | try { 124 | $this->state->setAreaCode(Area::AREA_GLOBAL); 125 | } catch (\Exception $e) { 126 | return Cli::RETURN_FAILURE; 127 | } 128 | 129 | $this->storeManager->setCurrentStore(0); 130 | 131 | $isUnlink = !($input->getOption('unlink') === 'false') && $input->getOption('unlink'); 132 | $isDryRun = !($input->getOption('dryrun') === 'false') && $input->getOption('dryrun'); 133 | 134 | $path = $this->directoryList->getPath('media'); 135 | 136 | $targetProductSku = $input->getArgument('products') ?: $this->getEntityIds(); 137 | $searchCriteriaBuilder = $this->searchCriteriaBuilder 138 | ->addFilter('image', 'no_selection', 'neq'); 139 | 140 | if ($input->getArgument('products')) { 141 | $searchCriteriaBuilder->addFilter('sku', $targetProductSku, 'in'); 142 | } else { 143 | $searchCriteriaBuilder->addFilter('entity_id', $this->getEntityIds(), 'in'); 144 | } 145 | 146 | $searchCriteriaBuilder = $searchCriteriaBuilder->create(); 147 | 148 | /** @var ProductSearchResultsInterface $products */ 149 | $products = $this->productRepository->getList($searchCriteriaBuilder); 150 | 151 | if (!$products->getTotalCount()) { 152 | return Cli::RETURN_SUCCESS; 153 | } 154 | 155 | if ($isDryRun) { 156 | $output->writeln('THIS IS A DRY-RUN, NO CHANGES WILL BE MADE!'); 157 | } 158 | $output->writeln(sprintf('%s products found with 2 images or more.', $products->getTotalCount())); 159 | 160 | foreach ($products->getItems() as $product) { 161 | $product->setStoreId(0); 162 | $md5Values = []; 163 | $baseImage = $product->getImage(); 164 | 165 | $filePath = $path . '/catalog/product' . $baseImage; 166 | if ($this->isFileExists($filePath)) { 167 | $md5Values[] = md5_file($filePath); 168 | } 169 | 170 | $gallery = $product->getMediaGalleryEntries(); 171 | 172 | $shouldSave = false; 173 | $filePaths = []; 174 | 175 | if (empty($gallery)) { 176 | continue; 177 | } 178 | 179 | foreach ($gallery as $key => $galleryImage) { 180 | if ($galleryImage->getFile() == $baseImage) { 181 | continue; 182 | } 183 | 184 | $filePath = $path . '/catalog/product' . $galleryImage->getFile(); 185 | 186 | if ($this->isFileExists($filePath)) { 187 | $md5 = md5_file($filePath); 188 | } else { 189 | continue; 190 | } 191 | 192 | if (in_array($md5, $md5Values)) { 193 | if (count($galleryImage->getTypes()) > 0) { 194 | continue; 195 | } 196 | unset($gallery[$key]); 197 | $filePaths[] = $filePath; 198 | $output->writeln(sprintf('Removed duplicate image from %s', $product->getSku())); 199 | $shouldSave = true; 200 | } else { 201 | $md5Values[] = $md5; 202 | } 203 | } 204 | 205 | if (!$isDryRun && $shouldSave) { 206 | $product->setMediaGalleryEntries($gallery); 207 | try { 208 | $this->productRepository->save($product); 209 | } catch (\Exception $e) { 210 | $output->writeln('Could not save product: ' . $e->getMessage()); 211 | } 212 | } 213 | 214 | foreach ($filePaths as $filePath) { 215 | if (!$this->isFile($filePath)) { 216 | continue; 217 | } 218 | 219 | if (!$isDryRun 220 | && $isUnlink 221 | && $shouldSave 222 | ) { 223 | try { 224 | $this->fileDriver->deleteFile($filePath); 225 | } catch (FileSystemException $e) { 226 | continue; 227 | } 228 | } 229 | 230 | if ($isUnlink 231 | && $shouldSave 232 | ) { 233 | $output->writeln('Deleted file: ' . $filePath); 234 | } 235 | } 236 | } 237 | 238 | if ($isDryRun) { 239 | $output->writeln('THIS WAS A DRY-RUN, NO CHANGES WERE MADE!'); 240 | } else { 241 | $output->writeln('Duplicate images are removed'); 242 | } 243 | 244 | return Cli::RETURN_SUCCESS; 245 | } 246 | 247 | /** 248 | * Get Entity IDs related 249 | * 250 | * @return array 251 | */ 252 | public function getEntityIds(): array 253 | { 254 | $connection = $this->resource->getConnection(); 255 | $tableName = $this->resource->getTableName('catalog_product_entity_media_gallery_value_to_entity'); 256 | 257 | $select = $connection->select() 258 | ->from($tableName, ['entity_id']) 259 | ->group('entity_id') 260 | ->having('COUNT(entity_id) >= 2'); 261 | 262 | return $connection->fetchCol($select); 263 | } 264 | 265 | /** 266 | * Is file exists 267 | * 268 | * @param string $path 269 | * @return bool 270 | */ 271 | protected function isFileExists(string $path): bool 272 | { 273 | try { 274 | $fileExists = $this->fileDriver->isExists($path); 275 | } catch (\Exception $exception) { 276 | $fileExists = false; 277 | } 278 | 279 | return $fileExists; 280 | } 281 | 282 | /*** 283 | * Tells whether the filename is a regular file 284 | * 285 | * @param string $path 286 | * @return bool 287 | */ 288 | protected function isFile(string $path): bool 289 | { 290 | try { 291 | $isFile = $this->fileDriver->isFile($path); 292 | } catch (\Exception $exception) { 293 | $isFile = false; 294 | } 295 | 296 | return $isFile; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Elgentos\RemoveDuplicateImage\Console\RemoveDuplicate 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/registration.php: -------------------------------------------------------------------------------- 1 |