Skip to content

Commit

Permalink
feature: akeneo purge command
Browse files Browse the repository at this point in the history
  • Loading branch information
p3pega committed Apr 12, 2024
1 parent a402ac5 commit e693801
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 0 deletions.
156 changes: 156 additions & 0 deletions src/Command/AkeneoFileStoragePruneCatalogCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php

namespace Gracious\AkeneoExtras\Command;

use Akeneo\Pim\Enrichment\Bundle\Elasticsearch\ProductQueryBuilderFactory;
use Akeneo\Pim\Enrichment\Component\Product\Model\ProductInterface;
use Akeneo\Pim\Enrichment\Component\Product\Model\ProductModelInterface;
use Akeneo\Pim\Enrichment\Component\Product\Query\ProductQueryBuilderInterface;
use Akeneo\Pim\Structure\Bundle\Doctrine\ORM\Repository\AttributeRepository;
use Akeneo\Tool\Component\FileStorage\FilesystemProvider;
use Akeneo\Tool\Component\FileStorage\Model\FileInfoInterface;
use Akeneo\Tool\Component\FileStorage\Repository\FileInfoRepositoryInterface;
use Akeneo\Tool\Component\StorageUtils\Remover\BulkRemoverInterface;
use Akeneo\Tool\Component\StorageUtils\Remover\RemoverInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

class AkeneoFileStoragePruneCatalogCommand extends Command
{
public function __construct(
private AttributeRepository $mediaAttributeRepository,
private ProductQueryBuilderFactory $productQueryBuilderFactory,
private ProductQueryBuilderFactory $productModelQueryBuilderFactory,
private EntityManagerInterface $entityManager,
private FilesystemProvider $filesystemProvider,
private FileInfoRepositoryInterface $fileInfoRepository,
private RemoverInterface $fileRemover,
private string $filesystemAlias,
) {
parent::__construct();
}

protected function configure(): void
{
$this
->setName('akeneo:file-storage:prune')
->setDescription('Remove unused files from the Akeneo catalog storage filesystem')
->setHelp('Remove unused files from filesystem. Product images can not be restored from history after that.');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
// TODO: ability to keep files used by versions that are not older than N days - useful only for EE/GE

$io = new SymfonyStyle($input, $output);
$io->title('Starting check for orphaned files');

$amountDeleted = 0;
$usedFiles = $this->getUsedFiles();

if (!empty($usedFiles)) {
$amountDeleted = $this->process($io, $this->filesystemAlias, $usedFiles, 1000);
}

$io->success('Done. Deleted '.$amountDeleted.' files.');
return Command::SUCCESS;
}

/**
* @return array<int, bool>
*/
protected function getUsedFiles(): array
{
$mediaAttributes = $this->mediaAttributeRepository->findMediaAttributeCodes();

$pqb = $this->productModelQueryBuilderFactory->create([]);
$usedModelFiles = $this->findUsedImages($pqb, $mediaAttributes);

$pqb = $this->productQueryBuilderFactory->create([]);
$usedProductFiles = $this->findUsedImages($pqb, $mediaAttributes);

return $usedModelFiles + $usedProductFiles;
}

/**
* @param ProductQueryBuilderInterface $pqb
* @param array<string> $mediaAttributes
* @return array<int, bool>
*/
protected function findUsedImages(ProductQueryBuilderInterface $pqb, array $mediaAttributes): array
{
$productsCursor = $pqb->execute();
$usedFiles = [];
/** @var ProductModelInterface|ProductInterface $product */
foreach ($productsCursor as $product) {
foreach ($mediaAttributes as $attribute) {
// TODO: handle all possible channels and locales
$val = $product->getValue($attribute);
$data = $val?->getData();
if ($data instanceof FileInfoInterface && !$data->isRemoved() && !isset($usedFiles[$data->getId()])) {
/** @var FileInfoInterface $data */
$usedFiles[$data->getId()] = true;
}
}
$this->entityManager->detach($product);
}
return $usedFiles;
}

/**
* @param SymfonyStyle $io
* @param string $storage
* @param array<int, bool> $usedFiles
* @param int $batchSize
* @return int
*/
protected function process(SymfonyStyle $io, string $storage, array $usedFiles, int $batchSize): int
{
$fs = $this->filesystemProvider->getFilesystem($storage);

$amountDeleted = 0;
$lastId = null;

do {
$qb = $this->fileInfoRepository->createQueryBuilder('f')
->select('f')
->where('f.storage = :storage')
->orderBy('f.id', Criteria::ASC)
->setMaxResults($batchSize)
->setParameter('storage', $storage)
;

if ($lastId !== null) {
$qb
->andWhere('f.id > :lastId')
->setParameter('lastId', $lastId)
;
}
/** @var Collection<FileInfoInterface> $fileInfos */
$fileInfos = $qb->getQuery()->getResult();
$fetchedCount = count($fileInfos);

foreach ($fileInfos as $fileInfo) {
if (!isset($usedFiles[$fileInfo->getId()])) {
try {
$fs->delete($fileInfo->getKey());
$this->fileRemover->remove($fileInfo);
$amountDeleted++;
} catch (\Throwable $e) {
$io->error("Could not remove file '{$fileInfo->getKey()}' due to reason: {$e->getMessage()}");
}
}
$this->entityManager->detach($fileInfo);
$lastId = $fileInfo->getId();
}
} while($fetchedCount > 0);

return $amountDeleted;
}
}
13 changes: 13 additions & 0 deletions src/Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,16 @@ services:
Gracious\AkeneoExtras\Command\PimMassDeleteSystemFlushCommand:
tags:
- { name: 'console.command' }

Gracious\AkeneoExtras\Command\AkeneoFileStoragePruneCatalogCommand:
arguments:
$mediaAttributeRepository: '@pim_catalog.repository.attribute'
$productQueryBuilderFactory: '@pim_catalog.query.product_query_builder_factory'
$productModelQueryBuilderFactory: '@pim_catalog.query.product_model_query_builder_factory'
$entityManager: '@doctrine.orm.entity_manager'
$filesystemProvider: '@akeneo_file_storage.file_storage.filesystem_provider'
$fileInfoRepository: '@akeneo_file_storage.repository.file_info'
$fileRemover: '@akeneo_file_storage.remover.file'
$filesystemAlias: 'catalogStorage'
tags:
- { name: 'console.command' }

0 comments on commit e693801

Please sign in to comment.