<?php
namespace Boldr\Cms\CmsBundle\Upload;
use Boldr\Cms\CmsBundle\Entity\Upload;
use Boldr\Cms\CmsBundle\Storage\StorageBucketInterface;
use Boldr\Cms\CmsBundle\Storage\StorageBucketFactoryInterface;
use Symfony\Component\HttpFoundation\{ Response, BinaryFileResponse };
use Symfony\Component\HttpFoundation\HeaderUtils;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Mime\MimeTypesInterface;
use Exception;
class UploadStorageManager
{
private array $mediaSizes;
/**
* Storage for "raw" uploaded files
*/
private StorageBucketInterface $uploadsStorageBucket;
/**
* Storage for resized, reformatted files
*/
private StorageBucketFactoryInterface $derivativesStorageBucketFactory;
private EntityManagerInterface $entityManager;
private ImageFormatter $imageFormatter;
private MimeTypesInterface $mimeTypes;
public function __construct(
array $mediaSizes,
StorageBucketInterface $uploadsStorageBucket,
StorageBucketFactoryInterface $derivativesStorageBucketFactory,
EntityManagerInterface $entityManager,
ImageFormatter $imageFormatter,
MimeTypesInterface $mimeTypes
)
{
$this->mediaSizes = $mediaSizes;
$this->uploadsStorageBucket = $uploadsStorageBucket;
$this->derivativesStorageBucketFactory = $derivativesStorageBucketFactory;
$this->entityManager = $entityManager;
$this->imageFormatter = $imageFormatter;
$this->mimeTypes = $mimeTypes;
}
/**
* Try to upload a file.
*/
public function upload(string $file, string $preferredPublicName): ?Upload
{
$storageIdentifier = $this->uploadsStorageBucket->store($file, $preferredPublicName);
if ($storageIdentifier === null)
{
return null;
}
$upload = new Upload;
$upload->setMimeType($this->mimeTypes->getMimeTypes(pathinfo($preferredPublicName, PATHINFO_EXTENSION))[0] ?? 'application/octet-stream');
$upload->setStorageIdentifier($storageIdentifier);
$upload->setPublicName($preferredPublicName);
$upload->setSize(filesize($file));
if (strpos($upload->getMimeType(), 'image/') === 0)
{
$upload->setType(Upload::TYPE_IMAGE);
if ($upload->getMimeType() !== 'image/svg+xml')
{
[$width, $height] = getimagesize($file);
$upload->setWidth($width);
$upload->setHeight($height);
}
}
if (strpos($upload->getMimeType(), 'audio/') === 0)
{
$upload->setType(Upload::TYPE_AUDIO);
}
if (strpos($upload->getMimeType(), 'video/') === 0)
{
$upload->setType(Upload::TYPE_VIDEO);
}
return $upload;
}
/**
* Creates a response that will download the file.
*/
public function download(Upload $upload, ?string $name = null): Response
{
$storageIdentifier = $upload->getStorageIdentifier();
$localPath = $this->uploadsStorageBucket->getLocalPath($storageIdentifier);
$isTempFile = false;
if (!$localPath)
{
$localPath = tempnam(sys_get_temp_dir(), 'tmpdownload');
$isTempFile = true;
$this->uploadsStorageBucket->copy($storageIdentifier, $localPath);
}
$response = new BinaryFileResponse($localPath);
$response->deleteFileAfterSend($isTempFile);
$disposition = HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_ATTACHMENT,
$name ?? $upload->getPublicName()
);
$response->headers->set('Content-Disposition', $disposition);
return $response;
}
/**
* Try to delete an upload.
*/
public function delete(Upload $upload): void
{
$this->uploadsStorageBucket->delete($upload->getStorageIdentifier());
foreach ($upload->getDerivatives() as $bucketName => $identifier)
{
$bucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
$bucket->delete($identifier);
}
$this->entityManager->remove($upload);
}
public function setPublicName(Upload $upload, string $publicName): void
{
$upload->setPublicName($publicName);
$newIdentifier = $this->uploadsStorageBucket->setPublicName($upload->getStorageIdentifier(), $publicName);
if ($newIdentifier !== null)
{
$upload->setStorageIdentifier($newIdentifier);
}
$newDerivatives = [];
foreach ($upload->getDerivatives() as $bucketName => $identifier)
{
$bucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
$newIdentifier = $bucket->setPublicName($identifier, $publicName);
$newDerivatives[$bucketName] = $newIdentifier ?? $identifier;
}
$upload->setDerivatives($newDerivatives);
}
/**
* @param string|null $format Format, or NULL if original format
*/
public function getImageUrl(Upload $upload, string $baseUrl, ?string $size = null, ?string $format = null, bool $regenerate = false): string
{
if ($upload->getType() !== Upload::TYPE_IMAGE)
{
throw new \Exception('UploadStorageManager::getImageUrl: Argument #1 ($upload) must be an Image upload.');
}
// Generate derivative and get bucket name
$bucketName = $this->generateDerivative($upload, $size, $format, $regenerate);
// Retrieve public URL from the appropriate bucket
$storageBucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
return $storageBucket->getPublicUrl($upload->getDerivatives()[$bucketName], $baseUrl);
}
public function getUploadPath(Upload $upload): string
{
$localPath = $this->uploadsStorageBucket->getLocalPath($upload->getStorageIdentifier());
if ($localPath === null)
{
$localPath = tempnam(sys_get_temp_dir(), 'boldrcms');
$this->uploadsStorageBucket->copy($upload->getStorageIdentifier(), $localPath);
}
return $localPath;
}
public function getUploadContents(Upload $upload): string
{
return $this->uploadsStorageBucket->getContents($upload->getStorageIdentifier());
}
/**
* @param string|null $format Format, or NULL if original format
*/
public function getImageContents(Upload $upload, string $baseUrl, ?string $size = null, ?string $format = null, bool $regenerate = false): string
{
if ($upload->getType() !== Upload::TYPE_IMAGE)
{
throw new \Exception('UploadStorageManager::getImageUrl: Argument #1 ($upload) must be an Image upload.');
}
// Generate derivative and get bucket name
$bucketName = $this->generateDerivative($upload, $size, $format, $regenerate);
// Retrieve public URL from the appropriate bucket
$storageBucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
return $storageBucket->getContents($upload->getDerivatives()[$bucketName], $baseUrl);
}
/**
* @param string|null $format Format, or NULL if original format
*/
public function getImagePath(Upload $upload, string $baseUrl, ?string $size = null, ?string $format = null, bool $regenerate = false): string
{
if ($upload->getType() !== Upload::TYPE_IMAGE)
{
throw new \Exception('UploadStorageManager::getImageUrl: Argument #1 ($upload) must be an Image upload.');
}
// Generate derivative and get bucket name
$bucketName = $this->generateDerivative($upload, $size, $format, $regenerate);
// Retrieve public URL from the appropriate bucket
$storageBucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
$localPath = $storageBucket->getLocalPath($upload->getDerivatives()[$bucketName]);
if ($localPath === null)
{
$localPath = tempnam(sys_get_temp_dir(), 'boldrcms');
$storageBucket->copy($upload->getDerivatives()[$bucketName], $localPath);
}
return $localPath;
}
/**
* @param string|null $format Format, or NULL if original format
*/
public function getPublicUrl(Upload $upload, string $baseUrl): string
{
// Retrieve public URL from the appropriate bucket
$bucketName = match ($upload->getType()) {
Upload::TYPE_IMAGE => 'image',
Upload::TYPE_VIDEO => 'video',
Upload::TYPE_AUDIO => 'audio',
Upload::TYPE_DOCUMENT => 'document'
};
$storageBucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
if (!isset($upload->getDerivatives()[$bucketName]))
{
$localPath = $this->uploadsStorageBucket->getLocalPath($upload->getStorageIdentifier());
if ($localPath !== null)
{
$identifier = $storageBucket->store($localPath, $upload->getPublicName());
}
else
{
$contents = $this->uploadsStorageBucket->getContents($upload->getStorageIdentifier());
$identifier = $storageBucket->storeContent($contents, $upload->getPublicName());
}
$upload->addDerivative($bucketName, $identifier);
$this->entityManager->flush();
}
return $storageBucket->getPublicUrl($upload->getDerivatives()[$bucketName], $baseUrl);
}
public function getImageBucketName(Upload $upload, ?string $size = null, ?string $format = null): ?string
{
if ($upload->getType() !== Upload::TYPE_IMAGE)
{
return 'public';
}
if ($format !== null && !strpos($format, 'image/') === 0)
{
throw new \Exception('Cannot convert image upload to non-image mime type.');
}
$originalFormat = explode('/', $upload->getMimeType(), 2)[1];
$originalFormat = explode('+', $originalFormat, 2)[0];
// SVG only comes in 1 size and cannot be converted to a different format
if ($originalFormat === 'svg')
{
return 'svg';
}
$newFormat = $format === null ? $originalFormat : substr($format, 6);
if ($size === null)
{
$size = 'full';
}
return $size . '/'. $newFormat;
}
/**
* Enqueue upload for derivative generation.
*
* @return bool Whether the derivative was enqueued
*/
public function enqueueDerivative(Upload $upload, ?string $size = null, ?string $format = null): bool
{
if ($upload->getMimeType() === 'image/svg+xml')
{
return false;
}
// Find the appropriate bucket name
$bucketName = $this->getImageBucketName($upload, $size, $format);
if ($bucketName === null)
{
return false;
}
if (isset($upload->getDerivatives()[$bucketName]))
{
return false;
}
$upload->enqueueDerivative($size, $format);
return true;
}
/**
* @param string|null $format Format, or NULL if original format
*/
public function generateDerivative(Upload $upload, ?string $size = null, ?string $format = null, bool $regenerate = false, bool &$alreadyExisted = null): string
{
// SVGs get special treatment: they are stored in a bucket "svg" and not resized, etc.
$isSvg = $upload->getMimeType() === 'image/svg+xml';
// Find the appropriate bucket name
$bucketName = $this->getImageBucketName($upload, $size, $format);
if ($bucketName === null)
{
throw new \Exception('Cannot generate derivative: no appropriate storage bucket.');
}
// Check if the derivative already exists
$derivatives = $upload->getDerivatives();
$alreadyExisted = isset($derivatives[$bucketName]);
// If the derivative does not exist yet, generate it
if (!$alreadyExisted || $regenerate)
{
$storageBucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
$identifier = $upload->getStorageIdentifier();
// If it is an SVG, copy the content to another storage
$sourceFile = $this->uploadsStorageBucket->getLocalPath($identifier);
if ($isSvg)
{
if ($sourceFile !== null)
{
$identifier = $storageBucket->store($sourceFile, $upload->getPublicName());
}
else
{
$contents = $this->uploadsStorageBucket->getContents($identifier);
$identifier = $storageBucket->storeContent($contents, $upload->getPublicName());
}
}
else
{
$removeSourceFile = false;
if ($sourceFile === null)
{
$sourceFile = tempnam(sys_get_temp_dir(), 'bcmsimage');
$removeSourceFile = true;
$success = $this->uploadsStorageBucket->copy($identifier, $sourceFile);
if (!$success)
{
throw new \Exception('Could not copy source image "'. $identifier . '" from storage for formatting.');
}
}
// Format image
$temporaryFile = tempnam(sys_get_temp_dir(), 'bcmsimage');
$imageFormatterOptions = $size === null ? null : $this->getImageFormatterOptions($size);
$extension = $this->imageFormatter->format($sourceFile, $temporaryFile, $imageFormatterOptions, $format);
$publicName = pathinfo($upload->getPublicName(), PATHINFO_FILENAME) .'.'. $extension;
// Store derivative on file storage
$identifier = $storageBucket->store($temporaryFile, $publicName);
@unlink($temporaryFile);
if ($removeSourceFile)
{
@unlink($sourceFile);
}
}
// Save identifier of derivative file in Upload entity
$derivatives[$bucketName] = $identifier;
$upload->setDerivatives($derivatives);
$this->entityManager->flush();
}
return $bucketName;
}
/**
* Gets the size of an image
*/
public function getImageDimensions(Upload $upload): array
{
if ($upload->getType() !== Upload::TYPE_IMAGE)
{
throw new \Exception('Upload is not an image');
}
$localPath = $this->uploadsStorageBucket->getLocalPath($upload->getStorageIdentifier());
if ($localPath !== null)
{
[$width, $height] = getimagesize($localPath);
return [$width, $height];
}
$temporaryFile = tempnam(sys_get_temp_dir(), 'bcmsimage');
$this->uploadsStorageBucket->copy($upload->getStorageIdentifier(), $temporaryFile);
[$width, $height] = getimagesize($temporaryFile);
unlink($temporaryFile);
return [$width, $height];
}
private function getImageFormatterOptions(string $sizeName): ImageFormatterOptions
{
if ($sizeName === 'boldr_cms_attachment_selector_thumbnail')
{
$sizeName = 'boldr_cms_upload_selector_thumbnail';
}
if ($sizeName === 'boldr_cms_attachment_selector_preview')
{
$sizeName = 'boldr_cms_upload_selector_preview';
}
if (!isset($this->mediaSizes[$sizeName]))
{
throw new \Exception('Unknown media size "'.$sizeName.'"');
}
$size = $this->mediaSizes[$sizeName];
$imageFormatterOptions = new ImageFormatterOptions($size['width'], $size['height']);
$imageFormatterOptions->setBehavior($size['behavior']);
if (isset($size['background']))
{
$imageFormatterOptions->setBackgroundColor($size['background']);
}
if (isset($size['overlay']))
{
$imageFormatterOptions->setOverlays([$size['overlay']]);
}
return $imageFormatterOptions;
}
}