diff --git a/modules/quant_tome/drush.services.yml b/modules/quant_tome/drush.services.yml new file mode 100644 index 00000000..631a6c3f --- /dev/null +++ b/modules/quant_tome/drush.services.yml @@ -0,0 +1,6 @@ +services: + quant_tome.commands: + class: \Drupal\quant_tome\Commands\QuantTomeCommands + arguments: ['@quant_tome.deploy_batch'] + tags: + - { name: drush.command } diff --git a/modules/quant_tome/quant_tome.info.yml b/modules/quant_tome/quant_tome.info.yml new file mode 100644 index 00000000..7e8df163 --- /dev/null +++ b/modules/quant_tome/quant_tome.info.yml @@ -0,0 +1,9 @@ +name: 'Quant Tome' +type: module +description: 'Deploy Tome static output to Quant.' +core: 8.x +core_version_requirement: ^8 || ^9 +package: Quant +dependencies: +- tome:tome_static +- quant:quant_api diff --git a/modules/quant_tome/quant_tome.services.yml b/modules/quant_tome/quant_tome.services.yml new file mode 100644 index 00000000..cf159ddd --- /dev/null +++ b/modules/quant_tome/quant_tome.services.yml @@ -0,0 +1,13 @@ +services: + quant_tome.redirect_subscriber: + class: Drupal\quant_tome\EventSubscriber\RedirectSubscriber + arguments: ['@tome_static.generator', '@file_system'] + tags: + - { name: event_subscriber } + quant_tome.deploy_batch: + class: Drupal\quant_tome\QuantTomeBatch + arguments: + - '@tome_static.generator' + - '@file_system' + - '@quant_api.client' + - '@queue' diff --git a/modules/quant_tome/src/Commands/QuantTomeCommands.php b/modules/quant_tome/src/Commands/QuantTomeCommands.php new file mode 100644 index 00000000..3355f483 --- /dev/null +++ b/modules/quant_tome/src/Commands/QuantTomeCommands.php @@ -0,0 +1,63 @@ +batch = $batch; + } + + /** + * Deploy a Tome static build to Quant. + * + * @command quant:tome:deploy + */ + public function deploy(array $options = ['threads' => 5]) { + $this->io()->writeln('Preparing Tome output for Quant...'); + + if (!$this->batch->checkConfig()) { + $this->io()->error('Cannot connect to the Quant API. Please check the Quant configuration.'); + return 1; + } + if (!$this->batch->checkBuild()) { + $this->io()->error('No Tome static build is available. Please run "drush tome:static".'); + return 1; + } + + $batch_builder = $this->batch->getBatch(); + batch_set($batch_builder->toArray()); + + $result = drush_backend_batch_process(); + + if (!empty($result['object'][0]['errors'])) { + $this->io()->error('Deploy failed. Please consult the error log for more information.'); + return 1; + } + + // Process the queue after the batch has collected it. + $quant_drush = new QuantDrushCommands(); + $quant_drush->message(['threads' => $options['threads']]); + } + +} diff --git a/modules/quant_tome/src/EventSubscriber/RedirectSubscriber.php b/modules/quant_tome/src/EventSubscriber/RedirectSubscriber.php new file mode 100644 index 00000000..b6e08286 --- /dev/null +++ b/modules/quant_tome/src/EventSubscriber/RedirectSubscriber.php @@ -0,0 +1,68 @@ +staticGenerator = $static_generator; + $this->fileSystem = $file_system; + } + + /** + * Reacts to a response event. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event. + */ + public function onResponse(FilterResponseEvent $event) { + $response = $event->getResponse(); + $request = $event->getRequest(); + if ($request->attributes->has(StaticGeneratorInterface::REQUEST_KEY) && $response instanceof RedirectResponse) { + $base_dir = $this->staticGenerator->getStaticDirectory(); + $this->fileSystem->prepareDirectory($base_dir, FileSystemInterface::CREATE_DIRECTORY); + file_put_contents("$base_dir/_redirects", $request->getPathInfo() . ' ' . $response->getTargetUrl() . "\n", FILE_APPEND); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[KernelEvents::RESPONSE][] = ['onResponse']; + return $events; + } + +} diff --git a/modules/quant_tome/src/QuantTomeBatch.php b/modules/quant_tome/src/QuantTomeBatch.php new file mode 100644 index 00000000..e439c2b4 --- /dev/null +++ b/modules/quant_tome/src/QuantTomeBatch.php @@ -0,0 +1,230 @@ +static = $static; + $this->fileSystem = $file_system; + $this->client = $client; + $this->queueFactory = $queue_factory; + } + + /** + * Check to see if Quant is configured correctly. + * + * @return bool + * If we can connect to the Quant API. + */ + public function checkConfig() { + return $this->client->ping(); + } + + /** + * Determine if Tome export exists. + * + * @return bool + * State of the export location. + */ + public function checkBuild() { + return file_exists($this->static->getStaticDirectory()); + } + + /** + * Generate the batch to seed Tome exports. + * + * @return \Drupal\Core\Batch\BatchBuilder + * A batch builder object. + */ + public function getBatch() { + $batch_builder = new BatchBuilder(); + $files = []; + + foreach ($this->fileSystem->scanDirectory($this->static->getStaticDirectory(), '/.*/') as $file) { + $files[] = $file->uri; + } + + foreach (array_chunk($files, 10) as $chunk) { + $batch_builder->addOperation([$this, 'getHashes'], [$chunk]); + } + + $batch_builder->addOperation([$this, 'checkRequiredFiles']); + return $batch_builder; + } + + /** + * Generate hashes of the files. + * + * Generate hashes as Quant's API would for the file content. This will reduce + * the number of files that we need to seed in the final batch operation. + * + * @todo Quant meta look up or local? + * + * @param array $files + * List of file URIs. + * @param array|\ArrayAccess &$context + * The batch context. + */ + public function getHashes(array $files, &$context) { + $file_hashes = []; + foreach ($files as $file) { + $file_hashes[$file] = md5(file_get_contents($file)); + } + + $context['results']['files'] = isset($context['results']['files']) ? $context['results']['files'] : []; + $context['results']['files'] = array_merge($context['results']['files'], $file_hashes); + } + + /** + * Processes the hashed records and generates the deploy batch. + * + * Takes the computed file hashes and evaluates which files need to be sent + * to Quant. Then, it creates another batch operation to seed the data. + * + * @param array|\ArrayAccess $context + * The batch context. + */ + public function checkRequiredFiles(&$context) { + $file_hashes = $context['results']['files']; + + $queue = $this->queueFactory->get('quant_seed_worker'); + $queue->deleteQueue(); + + foreach ($file_hashes as $file_path => $hash) { + if (strpos($file_path, 'redirect') > -1) { + if ($handle = fopen($file_path, 'r')) { + while (!feof($handle)) { + $line = fgets($handle); + $redirect = explode(' ', $line); + $source = trim($redirect[0]); + if (empty($source)) { + break; + } + // Only use the destination URI. + $destination = parse_url(trim($redirect[1]), PHP_URL_PATH); + $queue->createItem(new RedirectItem([ + 'source' => $source, + 'destination' => $destination, + 'status_code' => 301, + ])); + } + } + fclose($handle); + continue; + } + + $uri = $this->pathToUri($file_path); + $item = new RouteItem([ + 'route' => $uri, + 'uri' => $uri, + 'file_path' => $file_path, + ]); + + $queue->createItem($item); + } + } + + /** + * Convert the path to a URI. + * + * @param string $file_path + * The file path. + * + * @return string + * URI based on the file path. + */ + public function pathToUri($file_path) { + // Strip directory and index.html to match regular Quant processing. + $uri = str_replace($this->static->getStaticDirectory(), '', $file_path); + $uri = str_replace('/index.html', '', $uri); + return $uri; + } + + /** + * Deploy a file to Quant. + * + * @var \Drupal\quant\Plugin\QueueItem $item + * The file item to send to Quant API. + */ + public function deploy($item, array &$context) { + \Drupal::logger('quant_tome')->notice('Sending %s', [ + '%s' => $item->log(), + ]); + $item->send(); + } + + /** + * Finish deploy process. + * + * @param bool $success + * TRUE if batch successfully completed. + * @param array $context + * Batch context. + */ + public function finish($success, array &$context) { + if ($success) { + \Drupal::logger('quant_tome')->info('Complete!'); + } + else { + \Drupal::logger('quant_tome')->error('Failed to deploy all files, check the logs!'); + } + } + +} diff --git a/src/Plugin/QueueItem/RouteItem.php b/src/Plugin/QueueItem/RouteItem.php index 820103d8..56e8ad71 100644 --- a/src/Plugin/QueueItem/RouteItem.php +++ b/src/Plugin/QueueItem/RouteItem.php @@ -19,6 +19,20 @@ class RouteItem implements QuantQueueItemInterface { */ private $route; + /** + * URI for the file. + * + * @var string + */ + private $uri; + + /** + * Path to the file. + * + * @var string + */ + private $filePath; + /** * {@inheritdoc} */ @@ -29,12 +43,15 @@ public function __construct(array $data = []) { throw new \UnexpectedValueException(self::class . ' requires a string value.'); } - // Ensure route starts with a slash. + // Ensure route starts with a slash and has no empty spaces. if (substr($route, 0, 1) != '/') { $route = "/{$route}"; } + $route = trim($route); - $this->route = trim($route); + $this->route = $route; + $this->uri = isset($data['uri']) ? $data['uri'] : strtok($route, '?'); + $this->filePath = isset($data['file_path']) ? $data['file_path'] : DRUPAL_ROOT . strtok($route, '?'); } /** @@ -43,19 +60,27 @@ public function __construct(array $data = []) { public function send() { // Wrapper for routes that resolve as files. - $ext = pathinfo(strtok($this->route, '?'), PATHINFO_EXTENSION); - - if ($ext && file_exists(DRUPAL_ROOT . strtok($this->route, '?'))) { - $file_item = new FileItem([ - 'file' => strtok($this->route, '?'), - 'url' => $this->route, - 'full_path' => DRUPAL_ROOT . $this->route, - ]); - $file_item->send(); - return; + $extension = pathinfo($this->filePath, PATHINFO_EXTENSION); + $response = FALSE; + + if (file_exists($this->filePath) && !empty($extension)) { + if ($extension != 'html') { + $file_item = new FileItem([ + 'file' => $this->filePath, + 'url' => $this->uri, + ]); + $file_item->send(); + return; + } + // Get the content from the file. + $response = [ + file_get_contents($this->filePath), + 'text/html; charset=UTF-8', + ]; + } + else { + $response = Seed::markupFromRoute($this->route); } - - $response = Seed::markupFromRoute($this->route); if (!$response) { \Drupal::logger('quant_seed')->error("Unable to send {$this->route}");