Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve directory tree read performance #30629

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 46 additions & 57 deletions app/code/Magento/MediaGalleryUi/Model/Directories/GetDirectoryTree.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Exception\ValidatorException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\Read;
use Magento\Framework\Filesystem\Directory\ReadInterface;
use Magento\Framework\Filesystem\Glob;
use Magento\MediaGalleryApi\Api\IsPathExcludedInterface;

/**
* Build media gallery folder tree structure by path
* Build media gallery folder tree structure
*/
class GetDirectoryTree
{
Expand All @@ -28,15 +29,23 @@ class GetDirectoryTree
*/
private $isPathExcluded;

/**
* @var Glob
*/
private $glob;

/**
* @param Filesystem $filesystem
* @param Glob $glob
* @param IsPathExcludedInterface $isPathExcluded
*/
public function __construct(
Filesystem $filesystem,
Glob $glob,
IsPathExcludedInterface $isPathExcluded
) {
$this->filesystem = $filesystem;
$this->glob = $glob;
$this->isPathExcluded = $isPathExcluded;
}

Expand All @@ -48,81 +57,61 @@ public function __construct(
*/
public function execute(): array
{
$tree = [
'name' => 'root',
'path' => '/',
'children' => []
];
$directories = $this->getDirectories();
foreach ($directories as $idx => &$node) {
$node['children'] = [];
$result = $this->findParent($node, $tree);
$parent = &$result['treeNode'];

$parent['children'][] = &$directories[$idx];
}
return $tree['children'];
return $this->getDirectories();
}

/**
* Build directory tree array in format for jstree strandart
* Read media directories recursively and build directory tree array in the jstree format
*
* @param string $path
* @return array
* @throws ValidatorException
*/
private function getDirectories(): array
private function getDirectories(string $path = ''): array
{
$directories = [];

/** @var Read $directory */
$directory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA);

if (!$directory->isDirectory()) {
return $directories;
}

foreach ($directory->readRecursively() as $path) {
if (!$directory->isDirectory($path) || $this->isPathExcluded->execute($path)) {
continue;
$absolutePath = $this->getMediaDirectory()->getAbsolutePath($path);
foreach ($this->glob->glob(rtrim($absolutePath, '/') . '/*', Glob::GLOB_ONLYDIR) as $childPath) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Glob() class won't work with remote storages, all FS operations must go directly thought DriverInterface

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shiftedreality so how we can read only directories with DriverInterface ? there is no any filter for that, iterator will go over all files for example if we have 1 million files and one directory that we need to get, it will takes 30 min with DriverInterface, instead of 0.1 sec.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is clearly missing a param in search(). I would suggest adding an optional pattern parameter to pass into the glob

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@engcom-Foxtrot engcom-Foxtrot Nov 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is clearly missing a param in search(). I would suggest adding an optional pattern parameter to pass into the glob

Hello, @shiftedreality. Unfortunately, changing the interface method declaration is a backward-incompatible change, so we should avoid it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely, but we can do it within the next release 2.5.0, once the branch is created

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shiftedreality that's a lot of time, the main idea to allow users such as @perfpcs use the New Media Gallery in the next release.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Nazar65 , I really wish I could use the new media gallery but even with the file changes, it still does not work properly, nor does the Adobe Stock sign in. I had to switch back to the old media gallery just to be able to get anything done.

$relativePath = $this->getMediaDirectory()->getRelativePath($childPath);
if (!$this->isPathExcluded->execute($relativePath)) {
$directories[] = $this->getTreeNode($relativePath);
}

$pathArray = explode('/', $path);
$directories[] = [
'data' => count($pathArray) > 0 ? end($pathArray) : $path,
'attr' => ['id' => $path],
'metadata' => [
'path' => $path
],
'path_array' => $pathArray
];
}

return $directories;
}

/**
* Find parent directory
* Format tree node based on path (relative to media directory)
*
* @param array $node
* @param array $treeNode
* @param int $level
* @param string $path
* @return array
* @throws ValidatorException
*/
private function findParent(array &$node, array &$treeNode, int $level = 0): array
private function getTreeNode(string $path): array
{
$nodePathLength = count($node['path_array']);
$treeNodeParentLevel = $nodePathLength - 1;

$result = ['treeNode' => &$treeNode];

if ($nodePathLength <= 1 || $level > $treeNodeParentLevel) {
return $result;
}
$pathArray = explode('/', $path);
return [
'data' => count($pathArray) > 0 ? end($pathArray) : $path,
'attr' => [
'id' => $path
],
'metadata' => [
'path' => $path
],
'path_array' => $pathArray,
'children' => $this->getDirectories($path)
];
}

foreach ($treeNode['children'] as &$tnode) {
if ($node['path_array'][$level] === $tnode['path_array'][$level]) {
return $this->findParent($node, $tnode, $level + 1);
}
}
return $result;
/**
* Retrieve media directory with read access
*
* @return ReadInterface
*/
private function getMediaDirectory(): ReadInterface
{
return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\MediaGalleryUi\Test\Integration\Directories;

use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\WriteInterface;
use Magento\MediaGalleryUi\Model\Directories\GetDirectoryTree;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;

/**
* Integration test for GetDirectoryTree
*/
class GetDirectoryTreeTest extends TestCase
{
private const TEST_FOLDER_NAME = 'folder-tree-fixture-folder';
private const TEST_SUB_FOLDER_NAME = 'folder-tree-fixture-subfolder';

/**
* @var GetDirectoryTree
*/
private $getFolderTree;

/**
* @inheritdoc
*/
protected function setUp(): void
{
$this->getFolderTree = Bootstrap::getObjectManager()->get(GetDirectoryTree::class);
$this->getMediaDirectory()->create(self::TEST_FOLDER_NAME . '/' . self::TEST_SUB_FOLDER_NAME);
}

/**
* @inheritdoc
*/
protected function tearDown(): void
{
$this->getMediaDirectory()->delete(self::TEST_FOLDER_NAME);
}

/**
* Verify generated folder tree
*/
public function testExecute(): void
{
$nodeIsCreated = false;

foreach ($this->getFolderTree->execute() as $node) {
if ($node['data'] === self::TEST_FOLDER_NAME) {
$nodeIsCreated = true;
$this->assertEquals($this->getExpectedTreeNode(), $node);
}
}

$this->assertTrue($nodeIsCreated, 'Test folder is not included in generated folder tree.');
}

/**
* Get formatted expected tree node
*
* @return array
*/
private function getExpectedTreeNode(): array
{
return [
'data' => self::TEST_FOLDER_NAME,
'attr' => [
'id' => self::TEST_FOLDER_NAME,
],
'metadata' => [
'path' => self::TEST_FOLDER_NAME,
],
'path_array' => [
self::TEST_FOLDER_NAME
],
'children' => [
[
'data' => self::TEST_SUB_FOLDER_NAME,
'attr' => [
'id' => self::TEST_FOLDER_NAME . '/' . self::TEST_SUB_FOLDER_NAME,
],
'metadata' => [
'path' => self::TEST_FOLDER_NAME . '/' . self::TEST_SUB_FOLDER_NAME,
],
'path_array' => [
self::TEST_FOLDER_NAME,
self::TEST_SUB_FOLDER_NAME
],
'children' => []
]
]
];
}

/**
* Retrieve media directory with write access
*
* @return WriteInterface
*/
private function getMediaDirectory(): WriteInterface
{
return Bootstrap::getObjectManager()->get(Filesystem::class)->getDirectoryWrite(DirectoryList::MEDIA);
}
}