Skip to content

Commit 9afdf35

Browse files
committed
Merge branch 'MC-42330' of https://github.com/magento-l3/magento2ce into PR-2021-08-18
2 parents 44cc3ca + 0802d3e commit 9afdf35

File tree

6 files changed

+312
-69
lines changed

6 files changed

+312
-69
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
9+
<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
11+
<test name="AdminAwsS3ImportSimpleProductImagesDuplicationTest" extends="AdminImportSimpleProductImagesDuplicationTest">
12+
<annotations>
13+
<features value="AwsS3"/>
14+
<stories value="Import Products"/>
15+
<title value="S3 - Duplicated images should not be created if the CSV file is imported more than once"/>
16+
<description value="Duplicated images should not be created if the CSV file is imported more than once"/>
17+
<severity value="MAJOR"/>
18+
<testCaseId value="MC-42986"/>
19+
<useCaseId value="MC-42330"/>
20+
<group value="catalog_import_export"/>
21+
<group value="remote_storage_aws_s3"/>
22+
<group value="remote_storage_disabled"/>
23+
</annotations>
24+
25+
<before>
26+
<!-- Locally Copy Import Files to Unique Media Import Directory -->
27+
<helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="createDirectory" stepKey="createDirectoryForImportImages">
28+
<argument name="path">pub/media/import/test_image_duplication</argument>
29+
</helper>
30+
<helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="copy" stepKey="copyProductBaseImage">
31+
<argument name="source">dev/tests/acceptance/tests/_data/{{placeholderBaseImage.file}}</argument>
32+
<argument name="destination">pub/media/import/test_image_duplication/{{placeholderBaseImage.file}}</argument>
33+
</helper>
34+
<helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="copy" stepKey="copyProductSmallImage">
35+
<argument name="source">dev/tests/acceptance/tests/_data/{{placeholderSmallImage.file}}</argument>
36+
<argument name="destination">pub/media/import/test_image_duplication/{{placeholderSmallImage.file}}</argument>
37+
</helper>
38+
<helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="copy" stepKey="copyProductThumbImage">
39+
<argument name="source">dev/tests/acceptance/tests/_data/{{placeholderThumbnailImage.file}}</argument>
40+
<argument name="destination">pub/media/import/test_image_duplication/{{placeholderThumbnailImage.file}}</argument>
41+
</helper>
42+
43+
<!-- Enable AWS S3 Remote Storage & Sync -->
44+
<magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage" after="copyProductThumbImage"/>
45+
<magentoCLI command="remote-storage:sync" timeout="120" stepKey="syncRemoteStorage" after="enableRemoteStorage"/>
46+
47+
<!-- Copy to Import Directory in AWS S3 -->
48+
<helper class="Magento\AwsS3\Test\Mftf\Helper\S3FileAssertions" method="createDirectory" stepKey="createDirectoryForImportFilesInS3" after="syncRemoteStorage">
49+
<argument name="path">var/import/images/test_image_duplication</argument>
50+
</helper>
51+
<helper class="Magento\AwsS3\Test\Mftf\Helper\S3FileAssertions" method="copy" stepKey="copyProductBaseImageS3" after="createDirectoryForImportFilesInS3">
52+
<argument name="source">media/import/test_image_duplication/{{placeholderBaseImage.file}}</argument>
53+
<argument name="destination">var/import/images/test_image_duplication/{{placeholderBaseImage.file}}</argument>
54+
</helper>
55+
<helper class="Magento\AwsS3\Test\Mftf\Helper\S3FileAssertions" method="copy" stepKey="copyProductSmallImageS3" after="copyProductBaseImageS3">
56+
<argument name="source">media/import/test_image_duplication/{{placeholderSmallImage.file}}</argument>
57+
<argument name="destination">var/import/images/test_image_duplication/{{placeholderSmallImage.file}}</argument>
58+
</helper>
59+
<helper class="Magento\AwsS3\Test\Mftf\Helper\S3FileAssertions" method="copy" stepKey="copyProductThumbImageS3" after="copyProductSmallImageS3">
60+
<argument name="source">media/import/test_image_duplication/{{placeholderThumbnailImage.file}}</argument>
61+
<argument name="destination">var/import/images/test_image_duplication/{{placeholderThumbnailImage.file}}</argument>
62+
</helper>
63+
</before>
64+
65+
<after>
66+
<!-- Delete S3 Data -->
67+
<remove keyForRemoval="deleteProductImageDirectory"/>
68+
<helper class="Magento\AwsS3\Test\Mftf\Helper\S3FileAssertions" method="deleteDirectory" stepKey="deleteImportFilesDirectoryS3" after="deleteCategory">
69+
<argument name="path">media/import/test_image_duplication</argument>
70+
</helper>
71+
<helper class="Magento\AwsS3\Test\Mftf\Helper\S3FileAssertions" method="deleteDirectory" stepKey="deleteImportImagesFilesDirectoryS3" after="deleteImportFilesDirectoryS3">
72+
<argument name="path">var/import/images/test_image_duplication</argument>
73+
</helper>
74+
75+
<!-- Disable AWS S3 Remote Storage & Delete Local Data -->
76+
<magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage" after="logoutFromAdmin"/>
77+
<helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="deleteDirectory" stepKey="deleteImportFilesDirectoryLocal" after="disableRemoteStorage">
78+
<argument name="path">pub/media/import/test_image_duplication</argument>
79+
</helper>
80+
</after>
81+
</test>
82+
</tests>

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

Lines changed: 92 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Magento\Framework\Exception\NoSuchEntityException;
2424
use Magento\Framework\Filesystem;
2525
use Magento\Framework\Filesystem\Driver\File;
26+
use Magento\Framework\Filesystem\DriverPool;
2627
use Magento\Framework\Intl\DateTimeFactory;
2728
use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor;
2829
use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface;
@@ -767,11 +768,6 @@ class Product extends AbstractEntity
767768
*/
768769
private $linkProcessor;
769770

770-
/**
771-
* @var File
772-
*/
773-
private $fileDriver;
774-
775771
/**
776772
* @param \Magento\Framework\Json\Helper\Data $jsonHelper
777773
* @param \Magento\ImportExport\Helper\Data $importExportData
@@ -938,7 +934,6 @@ public function __construct(
938934
$this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance()->get(DateTimeFactory::class);
939935
$this->productRepository = $productRepository ?? ObjectManager::getInstance()
940936
->get(ProductRepositoryInterface::class);
941-
$this->fileDriver = $fileDriver ?: ObjectManager::getInstance()->get(File::class);
942937
}
943938

944939
/**
@@ -1580,7 +1575,6 @@ protected function _saveProducts()
15801575
$previousType = null;
15811576
$prevAttributeSet = null;
15821577

1583-
$importDir = $this->_mediaDirectory->getAbsolutePath($this->getUploader()->getTmpDir());
15841578
$existingImages = $this->getExistingImages($bunch);
15851579
$this->addImageHashes($existingImages);
15861580

@@ -1750,7 +1744,10 @@ protected function _saveProducts()
17501744
$position = 0;
17511745
foreach ($rowImages as $column => $columnImages) {
17521746
foreach ($columnImages as $columnImageKey => $columnImage) {
1753-
$uploadedFile = $this->getAlreadyExistedImage($rowExistingImages, $columnImage, $importDir);
1747+
$hash = filter_var($columnImage, FILTER_VALIDATE_URL)
1748+
? $this->getRemoteFileHash($columnImage)
1749+
: $this->getFileHash($this->joinFilePaths($this->getUploader()->getTmpDir(), $columnImage));
1750+
$uploadedFile = $this->findImageByHash($rowExistingImages, $hash);
17541751
if (!$uploadedFile && !isset($uploadedImages[$columnImage])) {
17551752
$uploadedFile = $this->uploadMediaFiles($columnImage);
17561753
$uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage);
@@ -1955,40 +1952,29 @@ protected function _saveProducts()
19551952
*
19561953
* @param string $path
19571954
* @return string
1955+
* @throws \Magento\Framework\Exception\FileSystemException
19581956
*/
19591957
private function getFileHash(string $path): string
19601958
{
1961-
return hash_file(self::HASH_ALGORITHM, $path);
1959+
$content = '';
1960+
if ($this->_mediaDirectory->isFile($path)
1961+
&& $this->_mediaDirectory->isReadable($path)
1962+
) {
1963+
$content = $this->_mediaDirectory->readFile($path);
1964+
}
1965+
return $content ? hash(self::HASH_ALGORITHM, $content) : '';
19621966
}
19631967

19641968
/**
1965-
* Returns existed image
1969+
* Returns hash for remote file
19661970
*
1967-
* @param array $imageRow
1968-
* @param string $columnImage
1969-
* @param string $importDir
1971+
* @param string $filename
19701972
* @return string
19711973
*/
1972-
private function getAlreadyExistedImage(array $imageRow, string $columnImage, string $importDir): string
1974+
private function getRemoteFileHash(string $filename): string
19731975
{
1974-
if (filter_var($columnImage, FILTER_VALIDATE_URL)) {
1975-
$hash = $this->getFileHash($columnImage);
1976-
} else {
1977-
$path = $importDir . DIRECTORY_SEPARATOR . $columnImage;
1978-
$hash = $this->isFileExists($path) ? $this->getFileHash($path) : '';
1979-
}
1980-
1981-
return array_reduce(
1982-
$imageRow,
1983-
function ($exists, $file) use ($hash) {
1984-
if (!$exists && isset($file['hash']) && $file['hash'] === $hash) {
1985-
return $file['value'];
1986-
}
1987-
1988-
return $exists;
1989-
},
1990-
''
1991-
);
1976+
$hash = hash_file(self::HASH_ALGORITHM, $filename);
1977+
return $hash !== false ? $hash : '';
19921978
}
19931979

19941980
/**
@@ -1999,38 +1985,19 @@ function ($exists, $file) use ($hash) {
19991985
*/
20001986
private function addImageHashes(array &$images): void
20011987
{
2002-
$productMediaPath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)
2003-
->getAbsolutePath(DIRECTORY_SEPARATOR . 'catalog' . DIRECTORY_SEPARATOR . 'product');
2004-
1988+
$productMediaPath = $this->getProductMediaPath();
20051989
foreach ($images as $storeId => $skus) {
20061990
foreach ($skus as $sku => $files) {
20071991
foreach ($files as $path => $file) {
2008-
if ($this->fileDriver->isExists($productMediaPath . $file['value'])) {
2009-
$fileName = $productMediaPath . $file['value'];
2010-
$images[$storeId][$sku][$path]['hash'] = $this->getFileHash($fileName);
1992+
$hash = $this->getFileHash($this->joinFilePaths($productMediaPath, $file['value']));
1993+
if ($hash) {
1994+
$images[$storeId][$sku][$path]['hash'] = $hash;
20111995
}
20121996
}
20131997
}
20141998
}
20151999
}
20162000

2017-
/**
2018-
* Is file exists
2019-
*
2020-
* @param string $path
2021-
* @return bool
2022-
*/
2023-
private function isFileExists(string $path): bool
2024-
{
2025-
try {
2026-
$fileExists = $this->fileDriver->isExists($path);
2027-
} catch (\Exception $exception) {
2028-
$fileExists = false;
2029-
}
2030-
2031-
return $fileExists;
2032-
}
2033-
20342001
/**
20352002
* Clears entries from Image Set and Row Data marked as no_selection
20362003
*
@@ -2214,23 +2181,15 @@ protected function _getUploader()
22142181

22152182
$fileUploader->init();
22162183

2217-
$dirConfig = DirectoryList::getDefaultConfig();
2218-
$dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH];
2219-
2220-
// make media folder a primary folder for media in external storages
2221-
if (!is_a($this->_mediaDirectory->getDriver(), File::class)) {
2222-
$dirAddon = DirectoryList::MEDIA;
2223-
}
2224-
22252184
$tmpPath = $this->getImportDir();
22262185

22272186
if (!$fileUploader->setTmpDir($tmpPath)) {
22282187
throw new LocalizedException(
22292188
__('File directory \'%1\' is not readable.', $tmpPath)
22302189
);
22312190
}
2232-
$destinationDir = "catalog/product";
2233-
$destinationPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath($destinationDir);
2191+
2192+
$destinationPath = $this->getProductMediaPath();
22342193

22352194
$this->_mediaDirectory->create($destinationPath);
22362195
if (!$fileUploader->setDestDir($destinationPath)) {
@@ -2284,11 +2243,11 @@ protected function uploadMediaFiles($fileName, $renameFileOff = false)
22842243
*/
22852244
private function getSystemFile($fileName)
22862245
{
2287-
$filePath = 'catalog' . DIRECTORY_SEPARATOR . 'product' . DIRECTORY_SEPARATOR . $fileName;
2288-
/** @var \Magento\Framework\Filesystem\Directory\ReadInterface $read */
2289-
$read = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA);
2246+
$filePath = $this->joinFilePaths($this->getProductMediaPath(), $fileName);
22902247

2291-
return $read->isExist($filePath) && $read->isReadable($filePath) ? $fileName : '';
2248+
return $this->_mediaDirectory->isFile($filePath) && $this->_mediaDirectory->isReadable($filePath)
2249+
? $fileName
2250+
: '';
22922251
}
22932252

22942253
/**
@@ -3280,4 +3239,68 @@ private function getRowExistingStockItem(array $rowData): StockItemInterface
32803239
$websiteId = $this->stockConfiguration->getDefaultScopeId();
32813240
return $this->stockRegistry->getStockItem($productId, $websiteId);
32823241
}
3242+
3243+
/**
3244+
* Returns image that matches the provided hash
3245+
*
3246+
* @param array $images
3247+
* @param string $hash
3248+
* @return string
3249+
*/
3250+
private function findImageByHash(array $images, string $hash): string
3251+
{
3252+
$value = '';
3253+
if ($hash) {
3254+
foreach ($images as $image) {
3255+
if (isset($image['hash']) && $image['hash'] === $hash) {
3256+
$value = $image['value'];
3257+
break;
3258+
}
3259+
}
3260+
}
3261+
return $value;
3262+
}
3263+
3264+
/**
3265+
* Returns product media
3266+
*
3267+
* @return string relative path to root folder
3268+
*/
3269+
private function getProductMediaPath(): string
3270+
{
3271+
return $this->joinFilePaths($this->getMediaBasePath(), 'catalog','product');
3272+
}
3273+
3274+
/**
3275+
* Returns media base path
3276+
*
3277+
* @return string relative path to root folder
3278+
*/
3279+
private function getMediaBasePath(): string
3280+
{
3281+
$mediaDir = !is_a($this->_mediaDirectory->getDriver(), File::class)
3282+
// make media folder a primary folder for media in external storages
3283+
? $this->filesystem->getDirectoryReadByPath(DirectoryList::MEDIA)
3284+
: $this->filesystem->getDirectoryRead(DirectoryList::MEDIA);
3285+
3286+
return $this->_mediaDirectory->getRelativePath($mediaDir->getAbsolutePath());
3287+
}
3288+
3289+
/**
3290+
* Joins two paths and remove redundant directory separator
3291+
*
3292+
* @param string ...$paths
3293+
* @return string
3294+
*/
3295+
private function joinFilePaths(...$paths): string
3296+
{
3297+
$result = '';
3298+
if ($paths) {
3299+
$result = rtrim(array_shift($paths), DIRECTORY_SEPARATOR);
3300+
foreach ($paths as $path) {
3301+
$result .= DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR);
3302+
}
3303+
}
3304+
return $result;
3305+
}
32833306
}

0 commit comments

Comments
 (0)