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); } } }