diff --git a/src/AbstractDocumentFactory.php b/src/AbstractDocumentFactory.php index 82ed814..8caf6fb 100644 --- a/src/AbstractDocumentFactory.php +++ b/src/AbstractDocumentFactory.php @@ -126,10 +126,10 @@ public function getDocument(bool $allowEmpty = false, bool $allowDuplicates = fa if (false !== $fileHash && !$allowDuplicates) { $existingDocument = $this->documentFinder->findOneByHashAndAlgorithm($fileHash, $this->getHashAlgorithm()); if (null !== $existingDocument) { - if ( - $existingDocument->isRaw() - && null !== $existingDownscaledDocument = $existingDocument->getDownscaledDocument() - ) { + /* + * If existing document is a RAW, serve its downscaled version + */ + if (null !== $existingDownscaledDocument = $existingDocument->getDownscaledDocument()) { $existingDocument = $existingDownscaledDocument; } if (null !== $this->folder) { @@ -139,7 +139,10 @@ public function getDocument(bool $allowEmpty = false, bool $allowDuplicates = fa $this->logger->info(sprintf( 'File %s already exists with same checksum, do not upload it twice.', $existingDocument->getFilename() - )); + ), [ + 'path' => $existingDocument->getMountPath(), + ]); + (new Filesystem())->remove($file->getPathname()); return $existingDocument; } diff --git a/src/DownscaleImageManager.php b/src/DownscaleImageManager.php index 9be1122..4ff0699 100644 --- a/src/DownscaleImageManager.php +++ b/src/DownscaleImageManager.php @@ -8,9 +8,9 @@ use Intervention\Image\Constraint; use Intervention\Image\Image; use Intervention\Image\ImageManager; -use League\Flysystem\FilesystemException; use League\Flysystem\FilesystemOperator; use Psr\Log\LoggerInterface; +use RZ\Roadiz\Documents\Models\AdvancedDocumentInterface; use RZ\Roadiz\Documents\Models\DocumentInterface; use RZ\Roadiz\Documents\Models\FileHashInterface; @@ -28,263 +28,266 @@ public function __construct( /** * Downscale document if needed, overriding raw document. - * - * @throws FilesystemException */ public function processAndOverrideDocument(?DocumentInterface $document = null): void { - if (null !== $document && $document->isLocal() && $this->maxPixelSize > 0) { - $mountPath = $document->getMountPath(); - if (null === $mountPath) { - return; - } - $processImage = $this->getDownscaledImage($this->imageManager->make( - $this->documentsStorage->readStream($mountPath) - )); - if (false !== $processImage) { - if ( - null !== $this->createDocumentFromImage($document, $processImage) - && null !== $this->logger - ) { - $this->logger->info( - 'Document has been downscaled.', - [ - 'path' => $mountPath, - ] - ); - } - } + if (!$this->isValidDocument($document)) { + return; + } + + $documentPath = $document->getMountPath(); + $processedImage = $this->getProcessedImage($documentPath); + + if (null === $processedImage) { + return; + } + + if (null !== $this->saveProcessedDocument($document, $processedImage)) { + $this->logDownscaling($documentPath, $document); } } /** * Downscale document if needed, keeping existing raw document. - * - * @throws FilesystemException */ public function processDocumentFromExistingRaw(?DocumentInterface $document = null): void { - if (null !== $document && $document->isLocal() && $this->maxPixelSize > 0) { - if (null !== $document->getRawDocument() && $document->getRawDocument()->isLocal()) { - $documentPath = $document->getRawDocument()->getMountPath(); - } else { - $documentPath = $document->getMountPath(); - } + if (!$this->isValidDocument($document)) { + return; + } - if (null === $documentPath) { - return; - } + if (null !== $document->getRawDocument() && $document->getRawDocument()->isLocal()) { + $documentPath = $document->getRawDocument()->getMountPath(); + } else { + $documentPath = $document->getMountPath(); + } - $documentStream = $this->documentsStorage->readStream($documentPath); + $processedImage = $this->getProcessedImage($documentPath); - if (false !== $processImage = $this->getDownscaledImage($this->imageManager->make($documentStream))) { - if ( - null !== $this->createDocumentFromImage($document, $processImage, true) - && null !== $this->logger - ) { - $this->logger->info('Document has been downscaled.', ['path' => $documentPath, 'entity' => $document]); - } - } + if (null === $processedImage) { + return; + } + + if (null !== $this->saveProcessedDocument($document, $processedImage, true)) { + $this->logDownscaling($documentPath, $document); } } + private function saveProcessedDocument( + DocumentInterface $document, + Image $processedImage, + bool $keepExistingRaw = false, + ): ?DocumentInterface { + if (!$keepExistingRaw) { + $this->removeOldRawDocument($document); + } + + if (null === $document->getRawDocument() || !$keepExistingRaw) { + return $this->storeNewProcessedImage($document, $processedImage); + } + + return $this->overwriteExistingProcessedImage($document, $processedImage); + } + + /** + * Retrieve and process an image if necessary. + */ + private function getProcessedImage(?string $documentPath): ?Image + { + if (null === $documentPath) { + return null; + } + + $documentStream = $this->documentsStorage->readStream($documentPath); + + return $this->resizeImageIfNeeded($this->imageManager->make($documentStream)); + } + /** * Get downscaled image if size is higher than limit, * returns original image if lower or if image is a GIF. */ - protected function getDownscaledImage(Image $processImage): ?Image + private function resizeImageIfNeeded(Image $image): ?Image { - if ( - 'image/gif' !== $processImage->mime() - && ($processImage->width() > $this->maxPixelSize || $processImage->height() > $this->maxPixelSize) - ) { - // prevent possible upsizing - $processImage->resize( - $this->maxPixelSize, - $this->maxPixelSize, - function (Constraint $constraint) { - $constraint->aspectRatio(); - $constraint->upsize(); - } - ); - - return $processImage; + if (!$this->doesImageSupportDownscaling($image)) { + return null; } - return null; + // prevent possible upsizing + return $image->resize( + $this->maxPixelSize, + $this->maxPixelSize, + function (Constraint $constraint) { + $constraint->aspectRatio(); + $constraint->upsize(); + } + ); } - protected function updateDocumentFileHash(DocumentInterface $document): void + /** + * Remove an old raw document if it exists. + */ + private function removeOldRawDocument(DocumentInterface $document): void { + if (null === $rawDocument = $document->getRawDocument()) { + return; + } /* - * We need to re-hash file after being downscaled + * When document already exists with a raw doc reference. + * We have to delete former raw document before creating a new one. + * Keeping the same document to preserve existing relationships!! */ - if ( - $document instanceof FileHashInterface - && null !== $document->getFileHashAlgorithm() - ) { - /** @var DocumentInterface&FileHashInterface $document */ - $mountPath = $document->getMountPath(); - if (null === $mountPath) { - return; - } - $document->setFileHash($this->documentsStorage->checksum( - $mountPath, - ['checksum_algo' => $document->getFileHashAlgorithm()] - )); + $document->setRawDocument(null); + /* + * Make sure to disconnect raw document before removing it + * not to trigger Cascade deleting. + */ + $this->em->flush(); + $this->em->remove($rawDocument); + $this->em->flush(); + } + + /** + * Rename the original document as raw. + */ + private function renameOriginalAsRaw(DocumentInterface $document, DocumentInterface $rawDocument): bool + { + $originalPath = $document->getMountPath(); + $rawPath = $rawDocument->getMountPath(); + + if (!$originalPath || !$rawPath || !$this->documentsStorage->fileExists($originalPath)) { + return false; } + + $this->documentsStorage->move($originalPath, $rawPath); + + return true; } /** - * @throws FilesystemException + * Store a new processed image, renaming the original as raw. */ - protected function createDocumentFromImage( - DocumentInterface $originalDocument, - ?Image $processImage = null, - bool $keepExistingRaw = false, - ): ?DocumentInterface { + private function storeNewProcessedImage(DocumentInterface $document, Image $image): ?DocumentInterface + { + $rawDocument = clone $document; + $rawDocument->setFilename($this->generateRawFilename($document->getFilename())); + + if (!$this->renameOriginalAsRaw($document, $rawDocument)) { + return null; + } + + $this->writeNewProcessedImage($document, $image); + $document->setRawDocument($rawDocument); + $this->updateDocumentImageSize($document, $image); + $this->updateDocumentFileHash($document); + + $rawDocument->setRaw(true); + $this->em->persist($rawDocument); + $this->em->flush(); + + return $document; + } + + /** + * Write the processed image to the storage. + */ + private function writeNewProcessedImage(DocumentInterface $document, Image $image): void + { + $this->documentsStorage->write( + $document->getMountPath(), + $image->encode(null, 100)->getEncoded() + ); + } + + /** + * Write the processed image to the storage. + */ + private function updateDocumentImageSize(DocumentInterface $document, Image $image): void + { + if (!$document instanceof AdvancedDocumentInterface) { + return; + } + + $document->setImageWidth($image->width()); + $document->setImageHeight($image->height()); + $mountPath = $document->getMountPath(); + if (null === $mountPath) { + return; + } + $document->setFilesize($this->documentsStorage->fileSize($mountPath)); + } + + private function updateDocumentFileHash(DocumentInterface $document): void + { + /* + * We need to re-hash file after being downscaled + */ if ( - false === $keepExistingRaw - && null !== $formerRawDoc = $originalDocument->getRawDocument() + !$document instanceof FileHashInterface + || null === $document->getFileHashAlgorithm() ) { - /* - * When document already exists with a raw doc reference. - * We have to delete former raw document before creating a new one. - * Keeping the same document to preserve existing relationships!! - */ - $originalDocument->setRawDocument(null); - /* - * Make sure to disconnect raw document before removing it - * not to trigger Cascade deleting. - */ - $this->em->flush(); - $this->em->remove($formerRawDoc); - $this->em->flush(); + return; } - if (null === $originalDocument->getRawDocument() || false === $keepExistingRaw) { - if (null === $processImage) { - return $originalDocument; - } - /* - * We clone it to host raw document. - * Keeping the same document to preserve existing relationships!! - * - * Get every data from raw document. - */ - $rawDocument = clone $originalDocument; - $rawDocumentName = preg_replace( - '#\.(jpe?g|gif|tiff?|png|psd|webp|avif|heic|heif)$#', - $this->rawImageSuffix.'.$1', - $originalDocument->getFilename() - ); - if (null === $rawDocumentName) { - throw new \InvalidArgumentException('Raw document filename cannot be null'); - } - $rawDocument->setFilename($rawDocumentName); - $originalDocumentPath = $originalDocument->getMountPath(); - $rawDocumentPath = $rawDocument->getMountPath(); - - if ( - null !== $originalDocumentPath - && null !== $rawDocumentPath - && $this->documentsStorage->fileExists($originalDocumentPath) - && !$this->documentsStorage->fileExists($rawDocumentPath) - ) { - /* - * Original document path becomes raw document path. Rename it. - */ - $this->documentsStorage->move($originalDocumentPath, $rawDocumentPath); - /* - * Then save downscaled image as original document path. - */ - $this->documentsStorage->write( - $originalDocumentPath, - $processImage->encode(null, 100)->getEncoded() - ); - $originalDocument->setRawDocument($rawDocument); - - /* - * We need to re-hash file after being downscaled - */ - $this->updateDocumentFileHash($originalDocument); - $rawDocument->setRaw(true); - - $this->em->persist($rawDocument); - $this->em->flush(); - - return $originalDocument; - } + /** @var DocumentInterface&FileHashInterface $document */ + $mountPath = $document->getMountPath(); + if (null === $mountPath) { + return; + } + $document->setFileHash($this->documentsStorage->checksum( + $mountPath, + ['checksum_algo' => $document->getFileHashAlgorithm()] + )); + } - return null; - } elseif (null !== $processImage) { - /* - * New downscale document has been generated, we keep existing RAW document - * but we override downscaled file with the new one. - */ - $originalDocumentPath = $originalDocument->getMountPath(); - if (null === $originalDocumentPath) { - return null; - } - /* - * Remove existing downscaled document. - */ - $this->documentsStorage->delete($originalDocumentPath); - /* - * Then save downscaled image as original document path. - */ - $this->documentsStorage->write( - $originalDocumentPath, - $processImage->encode(null, 100)->getEncoded() - ); - /* - * We need to re-hash file after being downscaled - */ - $this->updateDocumentFileHash($originalDocument); - $this->em->flush(); - - return $originalDocument; - } else { - /* - * If raw document size is inside new maxSize cap - * we delete it and use it as new active document file. - */ - $rawDocument = $originalDocument->getRawDocument(); - if (null !== $rawDocument) { - $originalDocumentPath = $originalDocument->getMountPath(); - $rawDocumentPath = $rawDocument->getMountPath(); - - if (null === $originalDocumentPath || null === $rawDocumentPath) { - return null; - } - - /* - * Remove existing downscaled document. - */ - $this->documentsStorage->delete($originalDocumentPath); - $this->documentsStorage->move( - $rawDocumentPath, - $originalDocumentPath - ); - - /* - * Remove Raw document - */ - $originalDocument->setRawDocument(null); - /* - * We need to re-hash file after being downscaled - */ - $this->updateDocumentFileHash($originalDocument); - /* - * Make sure to disconnect raw document before removing it - * not to trigger Cascade deleting. - */ - $this->em->flush(); - $this->em->remove($rawDocument); - $this->em->flush(); - } + /** + * Overwrite an existing processed image. + */ + private function overwriteExistingProcessedImage(DocumentInterface $document, Image $image): DocumentInterface + { + $this->documentsStorage->delete($document->getMountPath()); + $this->writeNewProcessedImage($document, $image); + $this->updateDocumentImageSize($document, $image); + $this->updateDocumentFileHash($document); - return $originalDocument; - } + return $document; + } + + /** + * Generate a raw filename. + */ + private function generateRawFilename(string $filename): string + { + return preg_replace( + '#\.(jpe?g|gif|tiff?|png|psd|webp|avif|heic|heif)$#', + $this->rawImageSuffix.'.$1', + $filename + ) ?? throw new \InvalidArgumentException('Raw document filename cannot be null'); + } + + /** + * Check if a document is valid for processing. + */ + private function isValidDocument(?DocumentInterface $document): bool + { + return null !== $document && $document->isLocal() && $this->maxPixelSize > 0; + } + + /** + * Check if an image can be downscaled. + */ + private function doesImageSupportDownscaling(Image $image): bool + { + return 'image/gif' !== $image->mime() + && ($image->width() > $this->maxPixelSize || $image->height() > $this->maxPixelSize); + } + + /** + * Log a successful downscaling operation. + */ + private function logDownscaling(string $path, ?DocumentInterface $document = null): void + { + $context = ['path' => $path]; + $this->logger?->info('Document has been downscaled.', $context); } } diff --git a/src/Repository/DocumentRepositoryInterface.php b/src/Repository/DocumentRepositoryInterface.php index 833b8d4..3a02fa8 100644 --- a/src/Repository/DocumentRepositoryInterface.php +++ b/src/Repository/DocumentRepositoryInterface.php @@ -30,4 +30,9 @@ public function findDuplicates(): array; * @return array */ public function findAllWithoutFileHash(): array; + + /** + * @return T|null + */ + public function findOneByHashAndAlgorithm(string $hash, string $hashAlgorithm): ?DocumentInterface; }