view_mode_crop-1.0.x-dev/src/Controller/DownloadCropController.php
src/Controller/DownloadCropController.php
<?php
namespace Drupal\view_mode_crop\Controller;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Image\ImageFactory;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\file\FileInterface;
use Drupal\view_mode_crop\CropImageHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
* Controller to generate and deliver cropped images.
*/
class DownloadCropController extends ControllerBase {
/**
* The stream wrapper manager.
*
* @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
*/
protected $streamWrapperManager;
/**
* The lock backend.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* The image factory.
*
* @var \Drupal\Core\Image\ImageFactory
*/
protected $imageFactory;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* File system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* Constructor.
*
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
* The stream wrapper manager.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\Image\ImageFactory $image_factory
* The image factory.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The system service.
*/
public function __construct(StreamWrapperManagerInterface $streamWrapperManager, LockBackendInterface $lock, ImageFactory $image_factory, FileSystemInterface $file_system) {
$this->streamWrapperManager = $streamWrapperManager;
$this->lock = $lock;
$this->imageFactory = $image_factory;
$this->logger = $this->getLogger('view_mode_crop');
$this->fileSystem = $file_system;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('stream_wrapper_manager'),
$container->get('lock'),
$container->get('image.factory'),
$container->get('file_system'),
);
}
/**
* Generate and download a cropped image.
*
* @param string $entity_type_id
* The entity type id.
* @param string $entity_id
* The entity id.
* @param string $field_name
* The field name.
* @param string $delta
* The field delta.
* @param string $view_mode
* The view mode.
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response
* The cropped image.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\TypedData\Exception\MissingDataException
*/
public function download(string $entity_type_id, string $entity_id, string $field_name, string $delta, string $view_mode) {
$storage = $this->entityTypeManager()->getStorage($entity_type_id);
$entity = $storage->load($entity_id);
if (!$entity instanceof ContentEntityInterface) {
throw new \RuntimeException('Not a Content entity');
}
/**
* @var \Drupal\view_mode_crop\Plugin\Field\FieldType\ViewModeCropImageItem $image_item
*/
$image_item = $entity->get($field_name)->get($delta);
if (!$image_item->entity instanceof FileInterface) {
throw new \RuntimeException('Not a file entity');
}
$image_uri = $image_item->entity->_initialUri ?? $image_item->entity->getFileUri();
$derivative_uri = CropImageHelper::getUri($image_item, $delta, $view_mode);
$headers = [];
// Don't try to generate file if source is missing.
if (!file_exists($image_uri)) {
// If the image style converted the extension, it has been added to the
// original file, resulting in filenames like image.png.jpeg. So to find
// the actual source image, we remove the extension and check if that
// image exists.
$path_info = pathinfo(StreamWrapperManager::getTarget($image_uri));
$converted_image_uri = sprintf('%s://%s%s%s', $this->streamWrapperManager->getScheme($derivative_uri), $path_info['dirname'], DIRECTORY_SEPARATOR, $path_info['filename']);
if (!file_exists($converted_image_uri)) {
$this->logger->notice('Source image at %source_image_path not found while trying to generate derivative image at %derivative_path.', [
'%source_image_path' => $image_uri,
'%derivative_path' => $derivative_uri,
]);
return new Response($this->t('Error generating image, missing source file.'), 404);
}
else {
// The converted file does exist, use it as the source.
$image_uri = $converted_image_uri;
}
}
// Don't start generating the image if the derivative already exists or if
// generation is in progress in another thread.
if (!file_exists($derivative_uri)) {
$lock_name = 'view_mode_crop_download:' . $entity_type_id . ': ' . $entity_id . ':' . $field_name . ':' . $delta . ':' . Crypt::hashBase64($image_uri);
$lock_acquired = $this->lock->acquire($lock_name);
if (!$lock_acquired) {
// Tell client to retry again in 3 seconds. Currently no browsers are
// known to support Retry-After.
throw new ServiceUnavailableHttpException(3, 'Crop image generation in progress. Try again shortly.');
}
}
// Try to generate the image, unless another thread just did it while we
// were acquiring the lock. The image is generated in url_stat in
// the crop stream wrapper.
$success = file_exists($derivative_uri);
if (!empty($lock_acquired) && isset($lock_name)) {
$this->lock->release($lock_name);
}
if ($success) {
$image = $this->imageFactory->get($derivative_uri);
$uri = $image->getSource();
$headers += [
'Content-Type' => $image->getMimeType(),
'Content-Length' => $image->getFileSize(),
];
return new BinaryFileResponse($uri, 200, $headers, TRUE);
}
else {
$this->logger->notice('Unable to generate the derived image located at %path.', ['%path' => $derivative_uri]);
return new Response($this->t('Error generating image.'), 500);
}
}
}
