Browse Source

First commit.

Frederic G. MARAND 7 years ago
commit
840a739070

+ 37 - 0
reinstall.drush.inc

@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Export content into YAML files.
+ */
+
+use Drupal\Component\Serialization\Yaml;
+use Symfony\Component\Serializer\Serializer;
+
+/**
+ * Implements hook_drush_command().
+ */
+function reinstall_drush_command() {
+  $items['reinstall-export'] = [
+    'description' => 'Export site content.',
+    'arguments' => [],
+    'required-arguments' => 0,
+    'aliases' => ['rex'],
+    'options' => [],
+    'drupal dependencies' => [],
+  ];
+
+  return $items;
+}
+
+/**
+ * Drush callback for reinstall-export.
+ *
+ * @param string $entity_type
+ *   The entity type.
+ */
+function drush_reinstall_export($entity_type = NULL) {
+  /** @var \Drupal\reinstall\Dumper $dumper */
+  $dumper = \Drupal::service('reinstall.dumper');
+  $dumper->dump($entity_type);
+}

+ 11 - 0
reinstall.info.yml

@@ -0,0 +1,11 @@
+name: Reinstall
+type: module
+description: "Export the content of a site to flat files and import it again."
+core: 8.x
+package: OSInet
+php: 7.0
+dependencies:
+  - migrate
+  - migrate_plus
+  - migrate_tools
+  - serialization

+ 35 - 0
reinstall.install

@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * Remove migration tables.
+ *
+ * @param $table
+ */
+function reinstall_remove_migration_tables($table) {
+  $db = \Drupal::database();
+  $logger = \Drupal::logger('reinstall');
+
+  $db->schema()->dropTable('migrate_map_' . $table);
+  $logger->notice('Table migrate_map_@table dropped', [
+      '@table' => $table,
+  ]);
+
+  $db->schema()->dropTable('migrate_message_' . $table);
+  $logger->notice('Table migrate_message_@table dropped', [
+      '@table' => $table,
+  ]);
+}
+
+/**
+ * Implements hook_uninstall.
+ */
+function reinstall_uninstall() {
+  // Get all migrations.
+  $manager = \Drupal::service('plugin.manager.config_entity_migration');
+  $plugins = $manager->createInstances([]);
+
+  // Remove migration tables.
+  foreach ($plugins as $migration) {
+    reinstall_remove_migration_tables($migration->id());
+  }
+}

+ 12 - 0
reinstall.services.yml

@@ -0,0 +1,12 @@
+parameters:
+  reinstall.path: '../data'
+
+services:
+  reinstall.dumper:
+    class: 'Drupal\reinstall\Dumper'
+    arguments:
+      - '@account_switcher'
+      - '@entity_type.bundle.info'
+      - '@entity_type.manager'
+      - '@app.root'
+      - '@serializer'

+ 301 - 0
src/Dumper.php

@@ -0,0 +1,301 @@
+<?php
+
+namespace Drupal\reinstall;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Entity\ContentEntityStorageInterface;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfo;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Session\AccountSwitcherInterface;
+use Drupal\Core\Session\UserSession;
+use Drupal\file\Entity\File;
+use Symfony\Component\Serializer\Serializer;
+use Symfony\Component\Yaml\Yaml;
+
+/**
+ * Class Dumper provides export for content entities.
+ */
+class Dumper {
+
+  const CONTENT_INTERFACE = 'Drupal\Core\Entity\ContentEntityInterface';
+
+  /**
+   * The path to the data files to import, relative to app.root.
+   */
+  const IMPORT_PATH = '../data';
+
+  /**
+   * The structure depth at which the YAML dump switched to inline format.
+   */
+  const INLINE_DEPTH = 5;
+
+  /**
+   * The account_switcher service.
+   *
+   * @var \Drupal\Core\Session\AccountSwitcherInterface
+   */
+  protected $accountSwitcher;
+
+  /**
+   * The entity_type.bundle_info service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfo
+   */
+
+  protected $entityTypeBundleInfo;
+
+  /**
+   * The entity_type.manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The actual path from which to perform imports.
+   *
+   * Derived from @app_root and %rdcm.reinstall_path%.
+   *
+   * @var string
+   */
+  protected $importPath = self::IMPORT_PATH;
+
+  /**
+   * The app.root parameter service.
+   *
+   * @var string
+   */
+  protected $root;
+
+  /**
+   * The serializer service.
+   *
+   * @var \Symfony\Component\Serializer\Serializer
+   */
+  protected $serializer;
+
+  /**
+   * Dumper constructor.
+   *
+   * @param \Drupal\Core\Session\AccountSwitcherInterface $accountSwitcher
+   *   The account_switcher service.
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfo $bundleInfo
+   *   The entity_type.bundle_info service.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity_type.manager service.
+   * @param string $root
+   *   The value of the app.root "parameter service".
+   * @param \Symfony\Component\Serializer\Serializer $serializer
+   *   The serializer service.
+   */
+  public function __construct(
+    AccountSwitcherInterface $accountSwitcher,
+    EntityTypeBundleInfo $bundleInfo,
+    EntityTypeManagerInterface $entityTypeManager,
+    string $root,
+    Serializer $serializer,
+    string $importPath
+    ) {
+    $this->accountSwitcher = $accountSwitcher;
+    $this->entityTypeBundleInfo = $bundleInfo;
+    $this->entityTypeManager = $entityTypeManager;
+    $this->root = $root;
+    $this->serializer = $serializer;
+
+    $this->setPath($importPath);
+  }
+
+  /**
+   * Gets a hash of the content entity definitions on the site.
+   *
+   * @param string $requestedTypeName
+   *   If specified, the types hash will only contain the key for that type.
+   *
+   * @return array
+   *   A machine-name-indexed hash of entity type definitions. If $typeName was
+   *   not specified, the definitions are returned for all entity types.
+   */
+  public function contentEntityTypes(string $requestedTypeName = NULL) {
+    $definitions = $this->entityTypeManager->getDefinitions();
+    $entityTypes = [];
+    foreach ($definitions as $machine => $type) {
+      $class = $type->getClass();
+      $implements = class_implements($class);
+      if (isset($implements[static::CONTENT_INTERFACE])) {
+        $entityTypes[$machine] = $type;
+      }
+    }
+
+    if (!empty($requestedTypeName)) {
+      $entityTypes = [$requestedTypeName => $entityTypes[$requestedTypeName]];
+    }
+
+    return $entityTypes;
+  }
+
+  /**
+   * Load entities for a given entity bundle.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $type
+   *   The entity type object.
+   * @param \Drupal\Core\Entity\ContentEntityStorageInterface $storage
+   *   The entity storage for the entity type.
+   *
+   * @return array
+   *   A hash of entities by id.
+   */
+  protected function loadMultiBundleEntities(ContentEntityTypeInterface $type,
+    ContentEntityStorageInterface $storage
+  ) {
+    $bundleNames = array_keys($this->entityTypeBundleInfo->getBundleInfo($type->id()));
+    $key = $type->getKey('bundle');
+
+    $entities = [];
+    foreach ($bundleNames as $bundleName) {
+      $bundleEntities = $storage->loadByProperties([$key => $bundleName]);
+      $entities[$bundleName] = $bundleEntities;
+    }
+
+    return $entities;
+  }
+
+  /**
+   * Dump entities to YAML files.
+   *
+   * @param string $requestedTypeName
+   *   Optional. The name of the entity type for which to export entities. If
+   *   absent, all entities in all types are dumped.
+   *
+   * @see https://www.drupal.org/node/218104
+   */
+  public function dump($requestedTypeName = NULL) {
+    $this->accountSwitcher->switchTo(new UserSession(['uid' => 1]));
+
+    $entityTypes = $this->contentEntityTypes($requestedTypeName);
+    foreach ($entityTypes as $entityTypeName => $entityType) {
+      $storage = $this->entityTypeManager->getStorage($entityTypeName);
+      $entities = $entityType->hasKey('bundle')
+        ? $this->loadMultiBundleEntities($entityType, $storage)
+        : [$entityTypeName => $storage->loadMultiple()];
+
+      foreach ($entities as $bundleName => $bundleEntities) {
+        $this->dumpEntities($bundleEntities, $entityTypeName, $bundleName);
+        if ($entityTypeName === 'file') {
+          $this->dumpFiles($bundleEntities, $bundleName);
+        }
+      }
+    }
+    $this->accountSwitcher->switchBack();
+  }
+
+  /**
+   * Generate import YAML for entities.
+   *
+   * @param array $entities
+   *   The entities.
+   * @param string $entity_type
+   *   The entity type.
+   * @param string $bundle
+   *   The bundle name.
+   */
+  public function dumpEntities(array $entities, string $entity_type, string $bundle = NULL) {
+    if (!$bundle) {
+      $bundle = $entity_type;
+    }
+
+    $json_options = [];
+    $json = $this->serializer->serialize($entities, 'json', $json_options);
+    $hash = json_decode($json, TRUE);
+
+    $importPath = $this->importPath;
+    $dir = "$importPath/$entity_type";
+    if (!file_exists($dir)) {
+      mkdir($dir, 0777, TRUE);
+    }
+
+    $path = "${dir}/${bundle}.yml";
+
+    file_put_contents($path, Yaml::dump($hash, static::INLINE_DEPTH, 2));
+  }
+
+  public function dumpFiles(array $files, string $bundle) {
+    $importPath = $this->importPath;
+    $dir = "$importPath/file";
+    $usedNamespaces = array_keys(array_reduce($files, [$this, 'namespacesReducer'], []));
+    $lists = [];
+
+    foreach ($usedNamespaces as $ns) {
+      // XXX Consider using \0 to support xargs: file names MAY contain spaces.
+      $path = "$dir/$ns.list.txt";
+
+      $nsDir = "$dir/$ns";
+      if (!is_dir($nsDir)) {
+        echo "Creating $nsDir\n";
+        mkdir($nsDir, 0777, TRUE);
+      }
+
+      // fopen() is in text mode by default.
+      $lists[$ns] = [
+        'dir' => $nsDir,
+        'handle' => fopen($path, 'w'),
+      ];
+    }
+
+    /**
+     * @var int $fid
+     * @var \Drupal\file\Entity\File $file
+     */
+    foreach ($files as $fid => $file) {
+      $uri = $file->getFileUri();
+      $target = file_uri_target($uri);
+      fputs($lists[$ns]['handle'], $target . "\n");
+      $dest = $lists[$ns]['dir'] . '/' . $target;
+
+      $dir = dirname($dest);
+      if (!is_dir($dir)) {
+        mkdir($dir, 0777, TRUE);
+      }
+      file_unmanaged_copy($uri, $dest, FILE_EXISTS_REPLACE);
+    }
+
+    foreach ($lists as $list) {
+      fclose($list['handle']);
+    }
+  }
+
+
+  /**
+   * array_reduce() callback to collect namespaces from file entities.
+   *
+   * @param string[] $accu
+   * @param \Drupal\file\Entity\File $fileItem
+   *
+   * @return string[]
+   *
+   * @see \Drupal\reinstall\Dumper::dumpFiles()
+   */
+  protected function namespacesReducer(array $accu, File $fileItem) {
+    $uri = $fileItem->getFileUri();
+    // Plain filenames without a namespace. Should not happen, but...
+    if (FALSE === ($len = Unicode::strpos($uri, '://'))) {
+      return $accu;
+    };
+
+    $namespace = Unicode::substr($uri, 0, $len);
+    $accu[$namespace] = TRUE;
+    return $accu;
+  }
+
+  public function setPath(string $path) {
+    $completePath = $this->root . '/' . $path;
+    $real = realpath($completePath);
+    if (!is_dir($real)) {
+      drupal_set_message("Non-existent base dump directory: $completePath.", "error");
+      throw new \InvalidArgumentException("Non-existent base dump directory: $completePath.");
+      return;
+    }
+
+    $this->importPath = $real;
+  }
+}

+ 71 - 0
src/Plugin/migrate/source/ReinstallTermSource.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\reinstall\Plugin\migrate\source;
+
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Row;
+
+/**
+ * Source plugin for terms from a YAML file.
+ *
+ * @MigrateSource(
+ *   id = "reinstall_terms"
+ * )
+ */
+class ReinstallTermSource extends SimpleSource {
+
+  /**
+   * Constructor.
+   */
+  public function __construct(
+    array $configuration,
+    string $pluginId,
+    array $pluginDefinition,
+    MigrationInterface $migration
+  ) {
+    parent::__construct($configuration, $pluginId, $pluginDefinition, $migration);
+    $rawRecords = array_map([$this, 'flattenRecord'], $this->initialParse($configuration));
+    $this->records = $rawRecords;
+  }
+
+  protected function flattenRecord($record) {
+    $row = new Row($record);
+    $this->flattenRow($row);
+    return $row->getSource();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fields() {
+    $ret = [
+      'changed'           => 'Modification timestamp',
+      'default_langcode'  => 'ISO 639 default language code',
+      'description'       => 'The entity description',
+      'langcode'          => 'ISO 639 language code',
+      'name'              => 'The term name (PK)',
+      'parent'            => 'Parent term ID',
+      'path'              => 'Optional path alias',
+      'tid'               => 'Taxonomy term ID',
+      'uuid'              => 'UUID',
+      'vid'               => 'Vocabulary ID',
+      'weight'            => 'Poids',
+    ];
+
+    return $ret;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIds() {
+    $ids = [
+      'tid' => [
+        'type' => 'integer',
+      ],
+    ];
+
+    return $ids;
+  }
+
+}

+ 79 - 0
src/Plugin/migrate/source/ReinstallUserSource.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\reinstall\Plugin\migrate\source;
+
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Row;
+
+/**
+ * Source plugin for terms from a YAML file.
+ *
+ * @MigrateSource(
+ *   id = "reinstall_users"
+ * )
+ */
+class ReinstallUserSource extends SimpleSource {
+
+  /**
+   * Constructor.
+   */
+  public function __construct(
+    array $configuration,
+    string $pluginId,
+    array $pluginDefinition,
+    MigrationInterface $migration
+  ) {
+    parent::__construct($configuration, $pluginId, $pluginDefinition, $migration);
+    $rawRecords = array_map([$this, 'flattenRecord'], $this->initialParse($configuration));
+    $records = array_filter($rawRecords, [$this, 'filter01']);
+    $this->records = $records;
+  }
+
+  protected function flattenRecord($record) {
+    $row = new Row($record);
+    $this->flattenRow($row);
+    return $row->getSource();
+  }
+
+  protected function filter01($record) {
+    return ($record['uid'] ?? 0) > 1;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fields() {
+    $ret = [
+      'uid'               => 'User ID',
+      'uuid'              => 'UUID',
+      'langcode'          => 'ISO 639 language code',
+      'preferred_langcode' => 'ISO 639 preferred language code',
+      'preferred_admin_langcode' => 'ISO 639 preferred administrative language code',
+      'name'              => 'The user name (PK)',
+      'mail'              => 'The current user e-mail address',
+      'timezone'          => 'The user timezone',
+      'status'            => 'The user status',
+      'access'            => 'Latest access timestamp',
+      'login'             => 'Latest login timestamp',
+      'init'              => 'Initial user e-mail address',
+      'created'           => 'User creation timestamp',
+      'changed'           => 'Modification timestamp',
+      'default_langcode'  => 'ISO 639 default language code',
+    ];
+
+    return $ret;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIds() {
+    $ids = [
+      'uid' => [
+        'type' => 'integer',
+      ],
+    ];
+    return $ids;
+  }
+
+}

+ 138 - 0
src/Plugin/migrate/source/SimpleSource.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace Drupal\reinstall\Plugin\migrate\source;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Row;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Yaml\Exception\ParseException;
+use Symfony\Component\Yaml\Yaml;
+
+abstract class SimpleSource extends SourcePluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The source records.
+   *
+   * @var array
+   */
+  protected $records;
+
+  public static function create(
+    ContainerInterface $container,
+    array $configuration,
+    $pluginId,
+    $pluginDefinition,
+    MigrationInterface $migration = NULL
+  ) {
+    $importPath = $container->getParameter('reinstall.path');
+    $configuration['importPath'] = $importPath;
+    return new static($configuration, $pluginId, $pluginDefinition, $migration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function doCount() {
+    return count($this->records);
+  }
+
+  /**
+   * Flatten a typical Drupal 8 field array to a 1-level array.
+   */
+  protected function flattenRow(Row $row) {
+    $source = $row->getSource();
+    foreach ($source as $key => &$item_list) {
+      if (is_scalar($item_list)) {
+        continue;
+      }
+      if (count($item_list) > 1) {
+        $item = $item_list;
+      }
+      else {
+        $item = reset($item_list);
+      }
+
+      if (isset($item['target_id'])) {
+        $value = $item['target_id'];
+      }
+      elseif (is_scalar($item) || (count($item) != 1 && !isset($item['width']) && !isset($item['pid']))) {
+        $value = $item;
+      }
+      elseif (isset($item['value'])) {
+        $value = $item['value'];
+      }
+      // Handle bundle['target_id']
+      // Exclude image field to keep metadata (alt / title)
+      elseif (isset($item['target_id']) && !isset($item['alt']) && !isset($item['title'])) {
+        $value = $item['target_id'];
+      }
+      elseif (isset($item['pid'])) {
+        $value = $item['alias'];
+      }
+      else {
+        $value = $item;
+      }
+
+      if (empty($item)) {
+        $value = NULL;
+      }
+      $row->setSourceProperty($key, $value);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function initializeIterator() {
+    return new \ArrayIterator($this->records);
+  }
+
+  /**
+   * Load then parse the file requested in configuration and return its records.
+   *
+   * @param array $configuration
+   *
+   * @param string $key
+   *   Optional. A top-level key for the source document. If empty, items will
+   *   be parsed from the root of the source document.
+   *
+   *
+   * @return mixed
+   *
+   * @throws \Drupal\migrate\MigrateException
+   */
+  protected function initialParse(array $configuration, string $key = NULL) {
+    $baseFilePath = $configuration['file'] ?? NULL;
+    $importPath = $configuration['importPath'] ?? NULL;
+    $filePath = realpath("$importPath/$baseFilePath");
+    if (!is_file($filePath) || !is_readable($filePath)) {
+      throw new MigrateException("${filePath} is not a readable file.");
+    }
+
+    try {
+      $raw = file_get_contents($filePath);
+      $data = Yaml::parse($raw);
+    } catch (ParseException $e) {
+      throw new MigrateException("Cannot parse the contents of ${filePath}.");
+    }
+
+    if ($key) {
+      return $data[$key] ?? [];
+    }
+
+    return $data ?? [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __toString() {
+    $current = $this->getIterator()->current();
+    $ret = json_encode($current, JSON_PRETTY_PRINT);
+    return $ret;
+  }
+
+}