FeaturesGenerationArchive.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. <?php
  2. namespace Drupal\features\Plugin\FeaturesGeneration;
  3. use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
  4. use Drupal\features\FeaturesGenerationMethodBase;
  5. use Drupal\Core\Archiver\ArchiveTar;
  6. use Drupal\Core\Form\FormStateInterface;
  7. use Drupal\features\FeaturesBundleInterface;
  8. use Drupal\features\Package;
  9. use Symfony\Component\DependencyInjection\ContainerInterface;
  10. /**
  11. * Class for generating a compressed archive of packages.
  12. *
  13. * @Plugin(
  14. * id = \Drupal\features\Plugin\FeaturesGeneration\FeaturesGenerationArchive::METHOD_ID,
  15. * weight = -2,
  16. * name = @Translation("Download Archive"),
  17. * description = @Translation("Generate packages and optional profile as a compressed archive for download."),
  18. * )
  19. */
  20. class FeaturesGenerationArchive extends FeaturesGenerationMethodBase implements ContainerFactoryPluginInterface {
  21. /**
  22. * The CSRF token generator.
  23. *
  24. * @var \Drupal\Core\Access\CsrfTokenGenerator
  25. */
  26. protected $csrfToken;
  27. /**
  28. * Creates a new FeaturesGenerationArchive instance.
  29. *
  30. * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
  31. * The CSRF token generator.
  32. */
  33. public function __construct(\Drupal\Core\Access\CsrfTokenGenerator $csrf_token) {
  34. $this->csrfToken = $csrf_token;
  35. }
  36. /**
  37. * {@inheritdoc}
  38. */
  39. public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  40. return new static(
  41. $container->get('csrf_token')
  42. );
  43. }
  44. /**
  45. * The package generation method id.
  46. */
  47. const METHOD_ID = 'archive';
  48. /**
  49. * The filename being written.
  50. *
  51. * @var string
  52. */
  53. protected $archiveName;
  54. /**
  55. * Reads and merges in existing files for a given package or profile.
  56. */
  57. protected function preparePackage(Package $package, array $existing_packages, FeaturesBundleInterface $bundle = NULL) {
  58. if (isset($existing_packages[$package->getMachineName()])) {
  59. $existing_directory = $existing_packages[$package->getMachineName()];
  60. // Scan for all files.
  61. $files = file_scan_directory($existing_directory, '/.*/');
  62. foreach ($files as $file) {
  63. // Skip files in the any existing configuration directory, as these
  64. // will be replaced.
  65. foreach (array_keys($this->featuresManager->getExtensionStorages()->getExtensionStorages()) as $directory) {
  66. if (strpos($file->uri, $directory) !== FALSE) {
  67. continue 2;
  68. }
  69. }
  70. // Merge in the info file.
  71. if ($file->name == $package->getMachineName() . '.info') {
  72. $files = $package->getFiles();
  73. $files['info']['string'] = $this->mergeInfoFile($package->getFiles()['info']['string'], $file->uri);
  74. $package->setFiles($files);
  75. }
  76. // Read in remaining files.
  77. else {
  78. // Determine if the file is within a subdirectory of the
  79. // extension's directory.
  80. $file_directory = dirname($file->uri);
  81. if ($file_directory !== $existing_directory) {
  82. $subdirectory = substr($file_directory, strlen($existing_directory) + 1);
  83. }
  84. else {
  85. $subdirectory = NULL;
  86. }
  87. $package->appendFile([
  88. 'filename' => $file->filename,
  89. 'subdirectory' => $subdirectory,
  90. 'string' => file_get_contents($file->uri)
  91. ]);
  92. }
  93. }
  94. }
  95. }
  96. /**
  97. * {@inheritdoc}
  98. */
  99. public function generate(array $packages = array(), FeaturesBundleInterface $bundle = NULL) {
  100. // If no packages were specified, get all packages.
  101. if (empty($packages)) {
  102. $packages = $this->featuresManager->getPackages();
  103. }
  104. // Determine the best name for the tar archive.
  105. // Single package export, so name by package name.
  106. if (count($packages) == 1) {
  107. $filename = current($packages)->getMachineName();
  108. }
  109. // Profile export, so name by profile.
  110. elseif (isset($bundle) && $bundle->isProfile()) {
  111. $filename = $bundle->getProfileName();
  112. }
  113. // Non-default bundle, so name by bundle.
  114. elseif (isset($bundle) && !$bundle->isDefault()) {
  115. $filename = $bundle->getMachineName();
  116. }
  117. // Set a fallback name.
  118. else {
  119. $filename = 'generated_features';
  120. }
  121. $return = [];
  122. $this->archiveName = $filename . '.tar.gz';
  123. $archive_name = file_directory_temp() . '/' . $this->archiveName;
  124. if (file_exists($archive_name)) {
  125. file_unmanaged_delete($archive_name);
  126. }
  127. $archiver = new ArchiveTar($archive_name);
  128. // Add package files.
  129. foreach ($packages as $package) {
  130. if (count($packages) == 1) {
  131. // Single module export, so don't generate entire modules dir structure.
  132. $package->setDirectory($package->getMachineName());
  133. }
  134. $this->generatePackage($return, $package, $archiver);
  135. }
  136. return $return;
  137. }
  138. /**
  139. * Writes a package or profile's files to an archive.
  140. *
  141. * @param array &$return
  142. * The return value, passed by reference.
  143. * @param \Drupal\features\Package $package
  144. * The package or profile.
  145. * @param ArchiveTar $archiver
  146. * The archiver.
  147. */
  148. protected function generatePackage(array &$return, Package $package, ArchiveTar $archiver) {
  149. $success = TRUE;
  150. foreach ($package->getFiles() as $file) {
  151. try {
  152. $this->generateFile($package->getDirectory(), $file, $archiver);
  153. }
  154. catch (\Exception $exception) {
  155. $this->failure($return, $package, $exception);
  156. $success = FALSE;
  157. break;
  158. }
  159. }
  160. if ($success) {
  161. $this->success($return, $package);
  162. }
  163. }
  164. /**
  165. * Registers a successful package or profile archive operation.
  166. *
  167. * @param array &$return
  168. * The return value, passed by reference.
  169. * @param \Drupal\features\Package $package
  170. * The package or profile.
  171. */
  172. protected function success(array &$return, Package $package) {
  173. $type = $package->getType() == 'module' ? $this->t('Package') : $this->t('Profile');
  174. $return[] = [
  175. 'success' => TRUE,
  176. // Archive writing doesn't merit a message, and if done through the UI
  177. // would appear on the subsequent page load.
  178. 'display' => FALSE,
  179. 'message' => '@type @package written to archive.',
  180. 'variables' => [
  181. '@type' => $type,
  182. '@package' => $package->getName(),
  183. ],
  184. ];
  185. }
  186. /**
  187. * Registers a failed package or profile archive operation.
  188. *
  189. * @param array &$return
  190. * The return value, passed by reference.
  191. * @param \Drupal\features\Package $package
  192. * The package or profile.
  193. * @param \Exception $exception
  194. * The exception object.
  195. * @param string $message
  196. * Error message when there isn't an Exception object.
  197. */
  198. protected function failure(array &$return, Package $package, \Exception $exception = NULL, $message = '') {
  199. $type = $package->getType() == 'module' ? $this->t('Package') : $this->t('Profile');
  200. $return[] = [
  201. 'success' => FALSE,
  202. // Archive writing doesn't merit a message, and if done through the UI
  203. // would appear on the subsequent page load.
  204. 'display' => FALSE,
  205. 'message' => '@type @package not written to archive. Error: @error.',
  206. 'variables' => [
  207. '@type' => $type,
  208. '@package' => $package->getName(),
  209. '@error' => isset($exception) ? $exception->getMessage() : $message,
  210. ],
  211. ];
  212. }
  213. /**
  214. * Writes a file to the file system, creating its directory as needed.
  215. *
  216. * @param string $directory
  217. * The extension's directory.
  218. * @param array $file
  219. * Array with the following keys:
  220. * - 'filename': the name of the file.
  221. * - 'subdirectory': any subdirectory of the file within the extension
  222. * directory.
  223. * - 'string': the contents of the file.
  224. * @param ArchiveTar $archiver
  225. * The archiver.
  226. *
  227. * @throws Exception
  228. */
  229. protected function generateFile($directory, array $file, ArchiveTar $archiver) {
  230. $filename = $directory;
  231. if (!empty($file['subdirectory'])) {
  232. $filename .= '/' . $file['subdirectory'];
  233. }
  234. $filename .= '/' . $file['filename'];
  235. // Set the mode to 0644 rather than the default of 0600.
  236. if ($archiver->addString($filename, $file['string'], FALSE, ['mode' => 0644]) === FALSE) {
  237. throw new \Exception($this->t('Failed to archive file @filename.', ['@filename' => $file['filename']]));
  238. }
  239. }
  240. /**
  241. * {@inheritdoc}
  242. */
  243. public function exportFormSubmit(array &$form, FormStateInterface $form_state) {
  244. // Redirect to the archive file download.
  245. $form_state->setRedirect('features.export_download', ['uri' => $this->archiveName, 'token' => $this->csrfToken->get($this->archiveName)]);
  246. }
  247. }