Skip to content

Commit c9fbde1

Browse files
authored
ENGCOM-8252: Product images are being duplicated on import #26713
2 parents d31cd74 + e1f7557 commit c9fbde1

File tree

3 files changed

+158
-39
lines changed

3 files changed

+158
-39
lines changed

app/code/Magento/CatalogImportExport/Model/Import/Product.php

Lines changed: 128 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Magento\Framework\Exception\LocalizedException;
2424
use Magento\Framework\Exception\NoSuchEntityException;
2525
use Magento\Framework\Filesystem;
26+
use Magento\Framework\Filesystem\Driver\File;
2627
use Magento\Framework\Intl\DateTimeFactory;
2728
use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor;
2829
use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface;
@@ -44,9 +45,10 @@
4445
* @SuppressWarnings(PHPMD.ExcessivePublicCount)
4546
* @since 100.0.2
4647
*/
47-
class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
48+
class Product extends AbstractEntity
4849
{
49-
const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types';
50+
public const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types';
51+
private const HASH_ALGORITHM = 'sha256';
5052

5153
/**
5254
* Size of bunch - part of products to save in one step.
@@ -766,6 +768,11 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
766768
*/
767769
private $linkProcessor;
768770

771+
/**
772+
* @var File
773+
*/
774+
private $fileDriver;
775+
769776
/**
770777
* @param \Magento\Framework\Json\Helper\Data $jsonHelper
771778
* @param \Magento\ImportExport\Helper\Data $importExportData
@@ -814,6 +821,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
814821
* @param StatusProcessor|null $statusProcessor
815822
* @param StockProcessor|null $stockProcessor
816823
* @param LinkProcessor|null $linkProcessor
824+
* @param File|null $fileDriver
817825
* @throws LocalizedException
818826
* @throws \Magento\Framework\Exception\FileSystemException
819827
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
@@ -866,7 +874,8 @@ public function __construct(
866874
ProductRepositoryInterface $productRepository = null,
867875
StatusProcessor $statusProcessor = null,
868876
StockProcessor $stockProcessor = null,
869-
LinkProcessor $linkProcessor = null
877+
LinkProcessor $linkProcessor = null,
878+
?File $fileDriver = null
870879
) {
871880
$this->_eventManager = $eventManager;
872881
$this->stockRegistry = $stockRegistry;
@@ -930,6 +939,7 @@ public function __construct(
930939
$this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance()->get(DateTimeFactory::class);
931940
$this->productRepository = $productRepository ?? ObjectManager::getInstance()
932941
->get(ProductRepositoryInterface::class);
942+
$this->fileDriver = $fileDriver ?: ObjectManager::getInstance()->get(File::class);
933943
}
934944

935945
/**
@@ -1570,7 +1580,10 @@ protected function _saveProducts()
15701580
$uploadedImages = [];
15711581
$previousType = null;
15721582
$prevAttributeSet = null;
1583+
1584+
$importDir = $this->_mediaDirectory->getAbsolutePath($this->getUploader()->getTmpDir());
15731585
$existingImages = $this->getExistingImages($bunch);
1586+
$this->addImageHashes($existingImages);
15741587

15751588
foreach ($bunch as $rowNum => $rowData) {
15761589
// reset category processor's failed categories array
@@ -1738,7 +1751,8 @@ protected function _saveProducts()
17381751
$position = 0;
17391752
foreach ($rowImages as $column => $columnImages) {
17401753
foreach ($columnImages as $columnImageKey => $columnImage) {
1741-
if (!isset($uploadedImages[$columnImage])) {
1754+
$uploadedFile = $this->getAlreadyExistedImage($rowExistingImages, $columnImage, $importDir);
1755+
if (!$uploadedFile && !isset($uploadedImages[$columnImage])) {
17421756
$uploadedFile = $this->uploadMediaFiles($columnImage);
17431757
$uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage);
17441758
if ($uploadedFile) {
@@ -1753,7 +1767,7 @@ protected function _saveProducts()
17531767
ProcessingError::ERROR_LEVEL_NOT_CRITICAL
17541768
);
17551769
}
1756-
} else {
1770+
} elseif (isset($uploadedImages[$columnImage])) {
17571771
$uploadedFile = $uploadedImages[$columnImage];
17581772
}
17591773

@@ -1782,8 +1796,7 @@ protected function _saveProducts()
17821796
}
17831797

17841798
if (isset($rowLabels[$column][$columnImageKey])
1785-
&& $rowLabels[$column][$columnImageKey] !=
1786-
$currentFileData['label']
1799+
&& $rowLabels[$column][$columnImageKey] !== $currentFileData['label']
17871800
) {
17881801
$labelsForUpdate[] = [
17891802
'label' => $rowLabels[$column][$columnImageKey],
@@ -1792,7 +1805,7 @@ protected function _saveProducts()
17921805
];
17931806
}
17941807
} else {
1795-
if ($column == self::COL_MEDIA_IMAGE) {
1808+
if ($column === self::COL_MEDIA_IMAGE) {
17961809
$rowData[$column][] = $uploadedFile;
17971810
}
17981811
$mediaGallery[$storeId][$rowSku][$uploadedFile] = [
@@ -1908,24 +1921,14 @@ protected function _saveProducts()
19081921
}
19091922
}
19101923

1911-
$this->saveProductEntity(
1912-
$entityRowsIn,
1913-
$entityRowsUp
1914-
)->_saveProductWebsites(
1915-
$this->websitesCache
1916-
)->_saveProductCategories(
1917-
$this->categoriesCache
1918-
)->_saveProductTierPrices(
1919-
$tierPrices
1920-
)->_saveMediaGallery(
1921-
$mediaGallery
1922-
)->_saveProductAttributes(
1923-
$attributes
1924-
)->updateMediaGalleryVisibility(
1925-
$imagesForChangeVisibility
1926-
)->updateMediaGalleryLabels(
1927-
$labelsForUpdate
1928-
);
1924+
$this->saveProductEntity($entityRowsIn, $entityRowsUp)
1925+
->_saveProductWebsites($this->websitesCache)
1926+
->_saveProductCategories($this->categoriesCache)
1927+
->_saveProductTierPrices($tierPrices)
1928+
->_saveMediaGallery($mediaGallery)
1929+
->_saveProductAttributes($attributes)
1930+
->updateMediaGalleryVisibility($imagesForChangeVisibility)
1931+
->updateMediaGalleryLabels($labelsForUpdate);
19291932

19301933
$this->_eventManager->dispatch(
19311934
'catalog_product_import_bunch_save_after',
@@ -1939,6 +1942,87 @@ protected function _saveProducts()
19391942

19401943
// phpcs:enable
19411944

1945+
/**
1946+
* Returns image hash by path
1947+
*
1948+
* @param string $path
1949+
* @return string
1950+
*/
1951+
private function getFileHash(string $path): string
1952+
{
1953+
return hash_file(self::HASH_ALGORITHM, $path);
1954+
}
1955+
1956+
/**
1957+
* Returns existed image
1958+
*
1959+
* @param array $imageRow
1960+
* @param string $columnImage
1961+
* @param string $importDir
1962+
* @return string
1963+
*/
1964+
private function getAlreadyExistedImage(array $imageRow, string $columnImage, string $importDir): string
1965+
{
1966+
if (filter_var($columnImage, FILTER_VALIDATE_URL)) {
1967+
$hash = $this->getFileHash($columnImage);
1968+
} else {
1969+
$path = $importDir . DS . $columnImage;
1970+
$hash = $this->isFileExists($path) ? $this->getFileHash($path) : '';
1971+
}
1972+
1973+
return array_reduce(
1974+
$imageRow,
1975+
function ($exists, $file) use ($hash) {
1976+
if (!$exists && isset($file['hash']) && $file['hash'] === $hash) {
1977+
return $file['value'];
1978+
}
1979+
1980+
return $exists;
1981+
},
1982+
''
1983+
);
1984+
}
1985+
1986+
/**
1987+
* Generate hashes for existing images for comparison with newly uploaded images.
1988+
*
1989+
* @param array $images
1990+
* @return void
1991+
*/
1992+
private function addImageHashes(array &$images): void
1993+
{
1994+
$productMediaPath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)
1995+
->getAbsolutePath(DS . 'catalog' . DS . 'product');
1996+
1997+
foreach ($images as $storeId => $skus) {
1998+
foreach ($skus as $sku => $files) {
1999+
foreach ($files as $path => $file) {
2000+
if ($this->fileDriver->isExists($productMediaPath . $file['value'])) {
2001+
$fileName = $productMediaPath . $file['value'];
2002+
$images[$storeId][$sku][$path]['hash'] = $this->getFileHash($fileName);
2003+
}
2004+
}
2005+
}
2006+
}
2007+
}
2008+
2009+
/**
2010+
* Is file exists
2011+
*
2012+
* @param string $path
2013+
* @return bool
2014+
*/
2015+
private function isFileExists(string $path): bool
2016+
{
2017+
try {
2018+
$fileExists = $this->fileDriver->isExists($path);
2019+
} catch (\Exception $exception) {
2020+
$fileExists = false;
2021+
}
2022+
2023+
return $fileExists;
2024+
}
2025+
19422026
/**
19432027
* Clears entries from Image Set and Row Data marked as no_selection
19442028
*
@@ -1950,9 +2034,8 @@ private function clearNoSelectionImages($rowImages, $rowData)
19502034
{
19512035
foreach ($rowImages as $column => $columnImages) {
19522036
foreach ($columnImages as $key => $image) {
1953-
if ($image == 'no_selection') {
1954-
unset($rowImages[$column][$key]);
1955-
unset($rowData[$column]);
2037+
if ($image === 'no_selection') {
2038+
unset($rowImages[$column][$key], $rowData[$column]);
19562039
}
19572040
}
19582041
}
@@ -2095,6 +2178,21 @@ protected function _saveProductTierPrices(array $tierPriceData)
20952178
return $this;
20962179
}
20972180

2181+
/**
2182+
* Returns the import directory if specified or a default import directory (media/import).
2183+
*
2184+
* @return string
2185+
*/
2186+
private function getImportDir(): string
2187+
{
2188+
$dirConfig = DirectoryList::getDefaultConfig();
2189+
$dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH];
2190+
2191+
return empty($this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR])
2192+
? $dirAddon . DS . $this->_mediaDirectory->getRelativePath('import')
2193+
: $this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR];
2194+
}
2195+
20982196
/**
20992197
* Returns an object for upload a media files
21002198
*
@@ -2111,11 +2209,7 @@ protected function _getUploader()
21112209
$dirConfig = DirectoryList::getDefaultConfig();
21122210
$dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH];
21132211

2114-
if (!empty($this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR])) {
2115-
$tmpPath = $this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR];
2116-
} else {
2117-
$tmpPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath('import');
2118-
}
2212+
$tmpPath = $this->getImportDir();
21192213

21202214
if (!$fileUploader->setTmpDir($tmpPath)) {
21212215
throw new LocalizedException(

dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use Magento\Store\Model\Store;
3535
use Magento\Store\Model\StoreManagerInterface;
3636
use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper;
37+
use Magento\TestFramework\Indexer\TestCase;
3738
use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection;
3839
use Psr\Log\LoggerInterface;
3940

@@ -50,8 +51,10 @@
5051
* @SuppressWarnings(PHPMD.ExcessivePublicCount)
5152
* phpcs:disable Generic.PHP.NoSilencedErrors, Generic.Metrics.NestingLevel, Magento2.Functions.StaticFunction
5253
*/
53-
class ProductTest extends \Magento\TestFramework\Indexer\TestCase
54+
class ProductTest extends TestCase
5455
{
56+
private const LONG_FILE_NAME_IMAGE = 'magento_long_image_name_magento_long_image_name_magento_long_image_name.jpg';
57+
5558
/**
5659
* @var \Magento\CatalogImportExport\Model\Import\Product
5760
*/
@@ -1029,13 +1032,12 @@ function (\Magento\Framework\DataObject $item) {
10291032
)
10301033
);
10311034

1032-
$this->importDataForMediaTest('import_media_additional_images.csv');
1035+
$this->importDataForMediaTest('import_media_additional_long_name_image.csv');
10331036
$product->cleanModelCache();
10341037
$product = $this->getProductBySku('simple_new');
10351038
$items = array_values($product->getMediaGalleryImages()->getItems());
1036-
$images[] = ['file' => '/m/a/magento_additional_image_three.jpg', 'label' => ''];
1037-
$images[] = ['file' => '/m/a/magento_additional_image_four.jpg', 'label' => ''];
1038-
$this->assertCount(7, $items);
1039+
$images[] = ['file' => '/m/a/' . self::LONG_FILE_NAME_IMAGE, 'label' => ''];
1040+
$this->assertCount(6, $items);
10391041
$this->assertEquals(
10401042
$images,
10411043
array_map(
@@ -1047,6 +1049,23 @@ function (\Magento\Framework\DataObject $item) {
10471049
);
10481050
}
10491051

1052+
/**
1053+
* Test import twice and check that image will not be duplicate
1054+
*
1055+
* @magentoDataFixture mediaImportImageFixture
1056+
* @return void
1057+
*/
1058+
public function testSaveMediaImageDuplicateImages(): void
1059+
{
1060+
$this->importDataForMediaTest('import_media.csv');
1061+
$imagesCount = count($this->getProductBySku('simple_new')->getMediaGalleryImages()->getItems());
1062+
1063+
// import the same file again
1064+
$this->importDataForMediaTest('import_media.csv');
1065+
1066+
$this->assertCount($imagesCount, $this->getProductBySku('simple_new')->getMediaGalleryImages()->getItems());
1067+
}
1068+
10501069
/**
10511070
* Test that errors occurred during importing images are logged.
10521071
*
@@ -1089,6 +1108,10 @@ public static function mediaImportImageFixture()
10891108
'source' => __DIR__ . '/../../../../Magento/Catalog/_files/magento_thumbnail.jpg',
10901109
'dest' => $dirPath . '/magento_thumbnail.jpg',
10911110
],
1111+
[
1112+
'source' => __DIR__ . '/../../../../Magento/Catalog/_files/' . self::LONG_FILE_NAME_IMAGE,
1113+
'dest' => $dirPath . '/' . self::LONG_FILE_NAME_IMAGE,
1114+
],
10921115
[
10931116
'source' => __DIR__ . '/_files/magento_additional_image_one.jpg',
10941117
'dest' => $dirPath . '/magento_additional_image_one.jpg',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
sku,additional_images
2+
simple_new,magento_long_image_name_magento_long_image_name_magento_long_image_name.jpg

0 commit comments

Comments
 (0)