├── 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 |