Skip to content
Snippets Groups Projects
Commit 22590de0 authored by mortona2k's avatar mortona2k Committed by Oleksandr Kuzava
Browse files

Issue #3345288: Provide a drush command to export/import content

parent 8a96713a
No related branches found
No related tags found
1 merge request!59Issue #3345288: Provide a drush command to export/import content
......@@ -71,6 +71,31 @@ Check a few examples of existing plugins at `src/Plugin/SingleContentSyncFieldPr
The plugin should include both import and export logic (and it's pretty much straightforward).
## Drush commands
You can use Drush commands to export and import your content.
### Export
To export content you can use `drush content:export`. By default, the command will export all entities of type `Node` at the following location: `docroot/sites/default/files/export/zip`.
You can customize the execution of the command by passing it some parameters and options.
The first parameter will change the entity types being exported (e.g. `taxonomy_term`, `block_content`, etc.).
The second parameter will specify an output path from docroot.
For example: `drush content:export block_content ./export-folder` will export all entities of type `block_content` in the `docroot/export-folder` directory (if the export-folder directory does not exist, a new one will be created).
The following options can also be passed to the command:
- `--translate` if used, the export will also contain the translated content
- `--assets` if used, the export will also contain all necessary assets
- `--all-content` if used, the export will contain all entities of all entity types
- `--dry-run` if used, the terminal will show an example output without performing the export
- `--entities` if used, only the entities passed (using entity id) will be in the export. Usage: `drush content:export --entities="1,4,7"`. if `--all-content` is used, it will take priority over this option.
### Import
To import content you can use `drush content:import`. The import command requires a `path` parameter to import content from.
The `path` parameter is a relative path to the docroot folder.
For example: `drush content:import export-folder/content-bulk-export.zip` will import the contents of a zip folder in the following location `docroot/export-folder/content-bulk-export.zip`.
## Documentation
Check out the guide to see the module’s overview and the guidelines for using it
......
services:
single_content_sync.commands:
class: \Drupal\single_content_sync\Commands\ContentSyncCommands
arguments:
- '@single_content_sync.exporter'
- '@single_content_sync.importer'
- '@single_content_sync.file_generator'
- '@messenger'
- '@single_content_sync.helper'
- '@single_content_sync.command_helper'
tags:
- { name: drush.command }
......@@ -61,3 +61,11 @@ services:
parent: default_plugin_manager
arguments:
- '@entity_type.manager'
single_content_sync.command_helper:
class: Drupal\single_content_sync\Utility\CommandHelper
arguments:
- '@single_content_sync.importer'
- '@config.factory'
- '@entity_type.manager'
- '@single_content_sync.helper'
- '@file_system'
<?php
namespace Drupal\single_content_sync\Commands;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\single_content_sync\ContentExporterInterface;
use Drupal\single_content_sync\ContentFileGeneratorInterface;
use Drupal\single_content_sync\ContentImporterInterface;
use Drupal\single_content_sync\ContentSyncHelperInterface;
use Drupal\single_content_sync\Utility\CommandHelperInterface;
use Drush\Commands\DrushCommands;
/**
* Defines the commands for exporting and importing content with drush.
*/
class ContentSyncCommands extends DrushCommands {
use StringTranslationTrait;
/**
* The content exporter service.
*
* @var \Drupal\single_content_sync\ContentExporterInterface
*/
protected ContentExporterInterface $contentExporter;
/**
* The content importer service.
*
* @var \Drupal\single_content_sync\ContentImporterInterface
*/
protected ContentImporterInterface $contentImporter;
/**
* The content file generator service.
*
* @var \Drupal\single_content_sync\ContentFileGeneratorInterface
*/
protected ContentFileGeneratorInterface $fileGenerator;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected MessengerInterface $messenger;
/**
* The content sync helper service.
*
* @var \Drupal\single_content_sync\ContentSyncHelperInterface
*/
protected ContentSyncHelperInterface $contentSyncHelper;
/**
* The command helper.
*
* @var \Drupal\single_content_sync\Utility\CommandHelperInterface
*/
protected CommandHelperInterface $commandHelper;
/**
* Constructor of ContentSyncCommands.
*
* @param \Drupal\single_content_sync\ContentExporterInterface $content_exporter
* The content exporter service.
* @param \Drupal\single_content_sync\ContentImporterInterface $content_importer
* The content importer service.
* @param \Drupal\single_content_sync\ContentFileGeneratorInterface $file_generator
* The content file generator service.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \Drupal\single_content_sync\ContentSyncHelperInterface $content_sync_helper
* The content sync helper.
* @param \Drupal\single_content_sync\Utility\CommandHelperInterface $command_helper
* The command helper.
*/
public function __construct(
ContentExporterInterface $content_exporter,
ContentImporterInterface $content_importer,
ContentFileGeneratorInterface $file_generator,
MessengerInterface $messenger,
ContentSyncHelperInterface $content_sync_helper,
CommandHelperInterface $command_helper
) {
parent::__construct();
$this->contentExporter = $content_exporter;
$this->contentImporter = $content_importer;
$this->fileGenerator = $file_generator;
$this->messenger = $messenger;
$this->contentSyncHelper = $content_sync_helper;
$this->commandHelper = $command_helper;
}
/**
* Export all content of a given entity type.
*
* By default, running the content:export command as is
* ('drush content:export'), all entities of type 'node'
* will be exported, if allowed in the site's configuration,
* and placed in the default export directory.
*
* @param string $entityType
* The entity type to export, e.g. 'node'. default is 'node'.
* @param string $outputPath
* A string with the path to the directory the content should be
* exported to. Relative to Drupal root. If nothing will be passed content
* will be exported to default export directory.
* @param array $options
* The options prefixed with -- to customize the execution of the command.
*
* @command content:export
*
* @option $translate Whether to include translations in the export.
* @option $assets Whether to include assets in the export.
* @option $all-content Will export all entity types.
* @option $dry-run Will run the command in 'dry-run mode' and will not export anything.
* @option $entities
* A comma separated string of entity id's to be exported.
* Combine with param $entityType in order to target the correct entities.
* if $all-content is used, it will take priority over this option.
*
* @usage content:export node /relative/output/path --entities="1,4,17" --translate --assets --all-content --dry-run
*/
public function exportEntitiesCommand(string $entityType = 'node', string $outputPath = '', array $options = [
'translate' => FALSE,
'assets' => FALSE,
'all-content' => FALSE,
'dry-run' => FALSE,
'entities' => NULL,
]): void {
[
'translate' => $include_translations,
'assets' => $include_assets,
'all-content' => $all_allowed_content,
'dry-run' => $is_dry_run,
'entities' => $entity_ids_to_export,
] = $options;
$output_dir = $this->commandHelper->getRealDirectory($outputPath);
// Create message to inform user how they are running the command.
$message = $this->commandHelper->createMessageWithFlags("\nExecuting drush content:export {$entityType} {$outputPath}", $options);
$this->output->write($message);
$is_dry_run && $this->output->writeln($this->t("This is a dry run. No content will be exported.\n"));
// Get the entities that will be exported.
$entities = $this->commandHelper->getEntitiesToExport($entityType, $all_allowed_content, $entity_ids_to_export);
// Abort command if there are disallowed entities.
if ($this->contentSyncHelper->containsDisallowedEntities($entities)) {
$this->messenger->addError($this->t("The export couldn't be completed since it contains disallowed content. Please check the configuration of the Single Content Sync module, or export only allowed content."));
return;
}
if ($is_dry_run) {
// Generate the correct file name for the dry run.
$file_name = count($entities) === 1 && !$include_assets
? "{$this->contentSyncHelper->generateContentFileName($entities[0])}.yml"
: sprintf('content-bulk-export-%s.zip', date('d_m_Y-H_i'));
// Get the correct output path.
$zip_path = "{$output_dir}/{$file_name}";
$this->messenger->addStatus($this->t('Successfully exported the content. You can find the exported file at the following location: @path', [
'@path' => $zip_path,
]));
return;
}
// Generate YAML file if there is only 1 content to export without assets,
// else, generate Zip file.
$file = count($entities) === 1 && !$include_assets
? $this->fileGenerator->generateYamlFile($entities[0], $include_translations)
: $this->fileGenerator->generateBulkZipFile($entities, $include_translations, $include_assets);
if (!empty($output_dir)) {
$this->contentSyncHelper->prepareFilesDirectory($output_dir);
$file_target = $this->commandHelper->moveFile($file, $output_dir, explode('://', $file->getFileUri(), 2)[1]);
}
else {
$file_target = explode('://', $file->getFileUri(), 2)[1];
}
$this->messenger->addStatus($this->t('Successfully exported the content. You can find the exported file at the following location: @path', [
'@path' => $file_target,
]));
}
/**
* Import content from a file at the given path.
*
* @param string $path
* The path to the file to import, relative to the docroot folder.
*
* @command content:import
*
* @usage content:import /path/to/file.zip
*/
public function importEntitiesCommand(string $path): void {
$message = $this->commandHelper->createMessageWithFlags("\nExecuting drush content:import {$path}");
$this->output->write($message);
$file_path = $this->commandHelper->getRealDirectory($path);
$file_info = pathinfo($file_path);
try {
if (file_exists($file_path)) {
$file_info['extension'] === 'zip' ? $this->commandHelper->commandZipImport($file_path) : $this->contentImporter->importFromFile($file_path);
$this->messenger->addStatus($this->t('Successfully imported the content.'));
}
else {
throw new \Exception("The file {$file_path} does not exist.");
}
}
catch (\Exception $e) {
$this->messenger->addError($e->getMessage());
}
}
}
......@@ -3,8 +3,8 @@
namespace Drupal\single_content_sync;
use Drupal\Core\Archiver\ArchiverInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\TempStore\PrivateTempStore;
use Drupal\file\FileInterface;
......
......@@ -249,4 +249,18 @@ class ContentSyncHelper implements ContentSyncHelperInterface {
return $this->configFactory->get('system.site')->get('uuid');
}
/**
* {@inheritdoc}
*/
public function containsDisallowedEntities(array $entities): bool {
$allowed_entity_types = $this->configFactory->get('single_content_sync.settings')->get('allowed_entity_types');
foreach ($entities as $entity) {
$entity_type_id = $entity->getEntityTypeId();
if (!isset($allowed_entity_types[$entity_type_id]) || ($allowed_entity_types[$entity_type_id] && !isset($allowed_entity_types[$entity_type_id][$entity->bundle()]))) {
return TRUE;
}
}
return FALSE;
}
}
......@@ -127,4 +127,13 @@ interface ContentSyncHelperInterface {
*/
public function getSiteUuid(): string;
/**
* Returns TRUE if the provided entities contain disallowed entities.
*
* @return bool
* Returns TRUE if the provided entities contain disallowed entities,
* else returns FALSE.
*/
public function containsDisallowedEntities(array $entities): bool;
}
......@@ -3,8 +3,8 @@
namespace Drupal\single_content_sync;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......
<?php
namespace Drupal\single_content_sync\Utility;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\file\FileInterface;
use Drupal\single_content_sync\ContentImporterInterface;
use Drupal\single_content_sync\ContentSyncHelperInterface;
/**
* Provides functionality to be used by CLI tools.
*/
class CommandHelper implements CommandHelperInterface {
/**
* The content importer service.
*
* @var \Drupal\single_content_sync\ContentImporterInterface
*/
protected ContentImporterInterface $contentImporter;
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected ConfigFactoryInterface $configFactory;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The content sync helper service.
*
* @var \Drupal\single_content_sync\ContentSyncHelperInterface
*/
protected ContentSyncHelperInterface $contentSyncHelper;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected FileSystemInterface $fileSystem;
/**
* Constructor of ContentSyncCommands.
*
* @param \Drupal\single_content_sync\ContentImporterInterface $content_importer
* The content importer service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\single_content_sync\ContentSyncHelperInterface $content_sync_helper
* The content sync helper.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system.
*/
public function __construct(
ContentImporterInterface $content_importer,
ConfigFactoryInterface $config_factory,
EntityTypeManagerInterface $entity_type_manager,
ContentSyncHelperInterface $content_sync_helper,
FileSystemInterface $file_system,
) {
$this->contentImporter = $content_importer;
$this->configFactory = $config_factory;
$this->entityTypeManager = $entity_type_manager;
$this->contentSyncHelper = $content_sync_helper;
$this->fileSystem = $file_system;
}
/**
* {@inheritDoc}
*/
public function createMessageWithFlags(string $message, array $options = []): string {
$include_translations = $options['translate'] ?? FALSE;
$include_assets = $options['assets'] ?? FALSE;
$all_allowed_content = $options['all-content'] ?? FALSE;
$is_dry_run = $options['dry-run'] ?? FALSE;
$entity_ids_to_export = $options['entities'] ?? NULL;
$flags = $include_translations ? ' --translate' : '';
$flags .= $include_assets ? ' --assets' : '';
$flags .= $all_allowed_content ? ' --all-content' : '';
$flags .= $is_dry_run ? ' --dry-run' : '';
$flags .= $entity_ids_to_export ? " --entities=\"{$entity_ids_to_export}\"" : '';
return "{$message}{$flags}\n\n";
}
/**
* {@inheritDoc}
*/
public function commandZipImport(string $file_path): void {
$this->contentImporter->importFromZip($file_path);
drush_backend_batch_process();
}
/**
* {@inheritDoc}
*/
public function getEntitiesToExport(string $entityType = 'node', bool $all_allowed_content = FALSE, string $entity_ids_to_export = NULL): array {
if ($all_allowed_content) {
$allowed_entity_types = $this->configFactory->get('single_content_sync.settings')->get('allowed_entity_types');
$entities = array_reduce($this->entityTypeManager->getDefinitions(), function ($carry, $entity_type) use ($allowed_entity_types) {
if (isset($allowed_entity_types[$entity_type->id()])) {
return array_merge($carry, $this->entityTypeManager->getStorage($entity_type->id())->loadMultiple());
}
return $carry;
}, []);
}
elseif ($entity_ids_to_export) {
$entities = $this->getSelectedEntities($entityType, $entity_ids_to_export);
}
else {
$entities = $this->entityTypeManager->getStorage($entityType)->loadMultiple();
}
return $entities;
}
/**
* {@inheritDoc}
*/
public function getSelectedEntities(string $entity_type, string $ids_to_export): array {
$entity_ids = explode(',', $ids_to_export);
$entities = [];
$invalid_ids = array_filter($entity_ids, function ($id) {
return !intval($id);
});
if (!empty($invalid_ids)) {
$err_out = implode(', ', $invalid_ids);
throw new \Exception("The export couldn't be completed because the --entities contain invalid ids: {$err_out}");
}
foreach ($entity_ids as $id) {
$entity = $this->entityTypeManager->getStorage($entity_type)
->load($id);
if (!$entity) {
throw new \Exception("The export couldn't be completed because the --entities contain invalid id: {$id}");
}
$entities[] = $entity;
}
return $entities;
}
/**
* {@inheritDoc}
*/
public function moveFile(FileInterface $file, string $output_dir, string $file_target): string {
if (!$output_dir) {
return $file_target;
}
$target_base_name = basename($file_target);
$moved_file_path = "{$output_dir}/{$target_base_name}";
$this->fileSystem->move($file->getFileUri(), $moved_file_path);
return $moved_file_path;
}
/**
* {@inheritDoc}
*/
public function getRealDirectory(string $output_path): string {
if (!$output_path) {
return '';
}
$grandparent_path = \Drupal::root();
if (substr($output_path, 0, strlen('./')) === './') {
$output_path = substr($output_path, 2);
}
$relative_dir = rtrim($output_path, '/');
$parent_count = substr_count($relative_dir, '../');
$grandparent_path = !!$parent_count ? dirname($grandparent_path, $parent_count) : $grandparent_path;
$trimmed_relative_dir = ltrim(str_replace('../', '', $relative_dir), '/');
$output_dir = "{$grandparent_path}/{$trimmed_relative_dir}";
return $output_dir;
}
}
<?php
namespace Drupal\single_content_sync\Utility;
use Drupal\file\FileInterface;
/**
* Creates an interface for command helper.
*/
interface CommandHelperInterface {
/**
* Append flags to a message.
*
* @param string $message
* The message to append flags to.
* @param array $options
* The options array with flags to append.
*/
public function createMessageWithFlags(string $message, array $options = []): string;
/**
* Import content from a zip file.
*
* @param string $file_path
* The path to the zip file to import.
*/
public function commandZipImport(string $file_path): void;
/**
* Get the entities to export.
*
* @param string $entityType
* The entity type to export, e.g. 'node'.
* @param bool $all_allowed_content
* Will export all entity types if set to TRUE.
* @param string|null $entity_ids_to_export
* A comma separated string of entity type ids to export, e.g. "1,2,5".
*
* @return array
* Returns an array of entities to export.
*/
public function getEntitiesToExport(string $entityType = 'node', bool $all_allowed_content = FALSE, string $entity_ids_to_export = NULL): array;
/**
* Get selected entities.
*
* Get an array of entities to export based on an entity type and
* an array of entity ids.
*
* @param string $entity_type
* The entity type to export, e.g. 'node'.
* @param string $ids_to_export
* A comma separated string of entity type ids to export, e.g. "1,2,5".
*
* @return array
* An array of entities.
*/
public function getSelectedEntities(string $entity_type, string $ids_to_export): array;
/**
* Move a file to directory.
*
* @param \Drupal\file\FileInterface $file
* The file to move.
* @param string $output_dir
* A relative path to move the file to.
* @param string $file_target
* The current location of the file.
*
* @return string
* The directory to which the file was moved.
*/
public function moveFile(FileInterface $file, string $output_dir, string $file_target): string;
/**
* Gets the real output directory based on a relative output path.
*
* @param string $output_path
* The relative ouput path.
*
* @return string
* A string with the real output directory.
*/
public function getRealDirectory(string $output_path): string;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment