htools-8.x-1.x-dev/modules/htools_migrate/src/Plugin/QueueWorker/DownloadRemoteFile.php
modules/htools_migrate/src/Plugin/QueueWorker/DownloadRemoteFile.php
<?php
namespace Drupal\htools_migrate\Plugin\QueueWorker;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\file\FileInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser;
use function count;
/**
* Implements a queue to download file from remote.
*
* @QueueWorker(
* id = "download_remote_file",
* title = @Translation("Download file from remote"),
* cron = {"time" = 30}
* )
*
* This plugin uses queue data to download a remote file and link it to an
* entity. The entity metadata is provided in the queue item data.
*
* Usage example:
*
* @code
* $queue->createItem([
* 'type' => 'node',
* 'bundle' => 'article',
* 'id' => 15,
* 'source' => 'http://my.cdn.com/files/awesome-image.png',
* 'field' => 'header_image',
* 'properties' => [
* 'alt' => 'Header image',
* 'title' => 'My node title',
* ],
* 'attempts' => 60,
* ]);
* @endcode
*
* The attempts is the number of failed download attempts after which the item
* will be discarded. Leave it empty or 0 to try just one time.
*/
class DownloadRemoteFile extends QueueWorkerBase implements ContainerFactoryPluginInterface {
/**
* The entity-type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The queue service.
*
* @var \Drupal\Core\Queue\QueueInterface
*/
protected $queue;
/**
* HTTP client service.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* Extension guesser.
*
* @var \Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser
*/
protected $extensionGuesser;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entityTypeManager, QueueFactory $queue, ClientInterface $http_client) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entityTypeManager;
$this->queue = $queue->get($plugin_id);
$this->httpClient = $http_client;
$this->extensionGuesser = ExtensionGuesser::getInstance();
$this->extensionGuesser->register(new MimeTypeExtensionGuesser());
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('queue'),
$container->get('http_client')
);
}
/**
* {@inheritdoc}
*/
public function processItem($data) {
if (!$this->validateData($data)) {
// If thr item is not valid log an exception and release the item.
watchdog_exception('cron', new \InvalidArgumentException(), 'Invalid item data supplied for %plugin: @data', [
'%plugin' => $this->getPluginId(),
'@data' => str_replace("\n", '', var_export($data, TRUE)),
]);
return;
}
$attempts = 0;
if (!empty((int) $data['attempts'])) {
$attempts = (int) $data['attempts'];
}
try {
$storage = $this->entityTypeManager->getStorage($data['type']);
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
if (!empty($data['id'])) {
$entity = $storage->load($data['id']);
}
else {
$entities = $storage->loadByProperties(['remote_source' => $data['source']]);
$entity = reset($entities);
}
if ($entity === NULL || empty($entity)) {
throw new \InvalidArgumentException('The entity does not exists.');
}
$file = $this->fetchFile($data['source'], $data['keep_original_file_name']);
if (!($file instanceof FileInterface)) {
throw new \RuntimeException('The file could not be downloaded. Check the logs for more information.');
}
if (!$entity->hasField($data['field'])) {
throw new \InvalidArgumentException('The field does not exists.');
}
$value = [
'target_id' => $file->id(),
];
$entity->get($data['field'])->setValue($value + $data['properties']);
$entity->save();
$attempts = 0;
} catch (GuzzleException $e) {
--$attempts;
$this->handleError($data, $e);
} catch (\Exception $e) {
--$attempts;
$this->handleError($data, $e);
} finally {
// Check if we have any attempts left.
if ($attempts > 0) {
$data['attempts'] = $attempts;
$this->queue->createItem($data);
}
}
}
/**
* Validate queue item data.
*
* @param mixed $data
* The queue item data.
*
* @return bool
* Returns TRUE if the data is valid.
*/
protected function validateData($data): bool {
$pattern = [
'type',
'bundle',
'id',
'source',
'field',
'properties',
'keep_original_file_name',
'attempts',
];
$keys = array_keys($data + ['attempts' => 0]);
return count($pattern) === count($keys) && !empty(array_intersect($pattern, $keys));
}
/**
* Fetch file from remote URL and stores it.
*
* @param string $url
* The file url.
*
* @return bool|\Drupal\file\FileInterface
* The file instance or FALSE if cannot be fetched.
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function fetchFile($url, $keep_original = FALSE) {
$response = $this->httpClient->request('GET', $url, ['verify' => FALSE]);
$path = file_build_uri('migrate');
$writable = \Drupal::service('file_system')
->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);;
if ($writable === FALSE) {
$e = new \RuntimeException('Cannot create or modify permissions for ' . $path . ' directory.');
watchdog_exception('migrate', $e);
return NULL;
}
if ($keep_original === FALSE) {
$path .= '/' . Crypt::hashBase64($url);
}
else {
$info = pathinfo($url);
$path .= '/' . $info['filename'];
}
$header = $response->getHeader('Content-Type');
$extension = $this->extensionGuesser->guess($header[0]);
if (!empty($extension)) {
$path .= '.' . $extension;
}
$data = (string) $response->getBody();
return file_save_data($data, $path, FileSystemInterface::EXISTS_REPLACE);
}
/**
* Handle exceptions.
*
* @param mixed $data
* The item data.
* @param \Exception|\GuzzleHttp\Exception\GuzzleException $e
* The exception instance.
*/
protected function handleError($data, $e): void {
watchdog_exception('cron', $e, 'Download of %file failed: @error', [
'%file' => $data['source'],
'@error' => $e->getMessage(),
]);
}
}
