Files
esun3d/app/code/Esun/Catalog/Model/SpecificationArchiveRebuilder.php
T
2026-09-15 10:35:56 +08:00

185 lines
7.3 KiB
PHP

<?php
declare(strict_types=1);
namespace Esun\Catalog\Model;
use Esun\Catalog\Model\ResourceModel\SpecificationFile\CollectionFactory as FileCollectionFactory;
use Esun\Catalog\Model\ResourceModel\SpecificationImport as SpecificationImportResource;
use Esun\Catalog\Model\ResourceModel\SpecificationImport\CollectionFactory as ImportCollectionFactory;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Filesystem;
class SpecificationArchiveRebuilder
{
public function __construct(
private readonly FileCollectionFactory $fileCollectionFactory,
private readonly ImportCollectionFactory $importCollectionFactory,
private readonly SpecificationImportFactory $importFactory,
private readonly SpecificationImportResource $importResource,
private readonly SpecificationMedia $media,
private readonly Filesystem $filesystem
) {
}
/**
* @return array{status:string,path:string,file_count:int,missing_count:int}
*/
public function rebuild(SpecificationBrand $brand, bool $force = false): array
{
if (!class_exists(\ZipArchive::class)) {
throw new LocalizedException(__('Archive rebuilding requires the PHP ZipArchive extension.'));
}
$brandId = (int)$brand->getId();
if ($brandId <= 0) {
throw new LocalizedException(__('The specification brand must be saved before rebuilding its archive.'));
}
$import = $this->getLatestImport($brandId);
$currentPath = $import ? (string)$import->getData('archive_file') : '';
if (!$force && $currentPath !== '' && $this->media->exists($currentPath)) {
return [
'status' => 'skipped',
'path' => $currentPath,
'file_count' => (int)$import->getData('file_count'),
'missing_count' => 0,
];
}
$brandDirectoryName = $this->sanitizeArchiveSegment((string)$brand->getData('brand_name'), 'brand-' . $brandId);
$archiveDirectory = SpecificationMedia::ARCHIVE_PATH . '/' . $brandId;
$archivePath = $archiveDirectory . '/' . $brandDirectoryName . '.zip';
if (!$force && $this->media->exists($archivePath)) {
$fileCount = (int)$this->fileCollectionFactory->create()
->addFieldToFilter('brand_id', $brandId)
->getSize();
$this->saveImport($import, $brandId, $archivePath, $fileCount);
return [
'status' => 'linked',
'path' => $archivePath,
'file_count' => $fileCount,
'missing_count' => 0,
];
}
$mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
$mediaDirectory->create($archiveDirectory);
$temporaryPath = $archiveDirectory . '/.rebuild-' . bin2hex(random_bytes(8)) . '.zip';
$zip = new \ZipArchive();
if ($zip->open($mediaDirectory->getAbsolutePath($temporaryPath), \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
throw new LocalizedException(__('Unable to create the specification archive for brand "%1".', $brand->getData('brand_name')));
}
$fileCount = 0;
$missingCount = 0;
$entryNames = [];
try {
try {
$collection = $this->fileCollectionFactory->create()
->addFieldToFilter('brand_id', $brandId)
->setOrder('model_name', 'ASC')
->setOrder('file_name', 'ASC');
foreach ($collection as $file) {
$filePath = (string)$file->getData('file_path');
if ($filePath === '' || !$this->media->exists($filePath)) {
$missingCount++;
continue;
}
$modelName = $this->sanitizeArchiveSegment((string)$file->getData('model_name'), 'model');
$fileName = $this->sanitizeArchiveSegment((string)$file->getData('file_name'), 'specification');
$entryName = $brandDirectoryName . '/' . $modelName . '/' . $fileName;
if (isset($entryNames[$entryName])) {
$entryName = $brandDirectoryName . '/' . $modelName . '/'
. pathinfo($fileName, PATHINFO_FILENAME) . '-' . (int)$file->getId()
. ($this->getExtension($fileName) !== '' ? '.' . $this->getExtension($fileName) : '');
}
if ($zip->addFile($this->media->getAbsolutePath($filePath), $entryName)) {
$entryNames[$entryName] = true;
$fileCount++;
} else {
$missingCount++;
}
}
} finally {
$closed = $zip->close();
}
} catch (\Throwable $exception) {
if ($mediaDirectory->isExist($temporaryPath)) {
$mediaDirectory->delete($temporaryPath);
}
throw $exception;
}
if (!$closed || $fileCount === 0) {
if ($mediaDirectory->isExist($temporaryPath)) {
$mediaDirectory->delete($temporaryPath);
}
throw new LocalizedException(
__('No readable specification files were available for brand "%1".', $brand->getData('brand_name'))
);
}
if ($mediaDirectory->isExist($archivePath)) {
$mediaDirectory->delete($archivePath);
}
$mediaDirectory->renameFile($temporaryPath, $archivePath);
if ($currentPath !== '' && $currentPath !== $archivePath) {
$this->media->delete($currentPath);
}
$this->saveImport($import, $brandId, $archivePath, $fileCount);
return [
'status' => 'rebuilt',
'path' => $archivePath,
'file_count' => $fileCount,
'missing_count' => $missingCount,
];
}
private function getLatestImport(int $brandId): ?SpecificationImport
{
$import = $this->importCollectionFactory->create()
->addFieldToFilter('brand_id', $brandId)
->setOrder('updated_at', 'DESC')
->setPageSize(1)
->getFirstItem();
return $import->getId() ? $import : null;
}
private function saveImport(
?SpecificationImport $import,
int $brandId,
string $archivePath,
int $fileCount
): void {
$import ??= $this->importFactory->create();
$import->setData('brand_id', $brandId);
$import->setData('archive_file', $archivePath);
$import->setData('file_count', $fileCount);
$import->setData('status', 1);
$this->importResource->save($import);
}
private function sanitizeArchiveSegment(string $value, string $fallback): string
{
$value = trim(str_replace(['/', '\\'], '_', $value));
$value = preg_replace('/[\x00-\x1F\x7F]+/u', '_', $value) ?: '';
$value = trim($value, " .\t\n\r\0\x0B");
return $value !== '' ? $value : $fallback;
}
private function getExtension(string $fileName): string
{
return (string)pathinfo($fileName, PATHINFO_EXTENSION);
}
}