diff --git a/core/modules/migrate/src/Plugin/migrate/process/Download.php b/core/modules/migrate/src/Plugin/migrate/process/Download.php new file mode 100644 index 0000000..08c9b57 --- /dev/null +++ b/core/modules/migrate/src/Plugin/migrate/process/Download.php @@ -0,0 +1,117 @@ + FALSE, + 'guzzle_options' => [], + ]; + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->fileSystem = $file_system; + $this->httpClient = $http_client; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('file_system'), + $container->get('http_client') + ); + } + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + // If we're stubbing a file entity, return a uri of NULL so it will get + // stubbed by the general process. + if ($row->isStub()) { + return NULL; + } + list($source, $destination) = $value; + + // Modify the destination filename if necessary. + $replace = !empty($this->configuration['rename']) ? + FILE_EXISTS_RENAME : + FILE_EXISTS_REPLACE; + $final_destination = file_destination($destination, $replace); + + // Try opening the file first, to avoid calling file_prepare_directory() + // unnecessarily. We're suppressing fopen() errors because we want to try + // to prepare the directory before we give up and fail. + $destination_stream = @fopen($final_destination, 'w'); + if (!$destination_stream) { + // If fopen didn't work, make sure there's a writable directory in place. + $dir = $this->fileSystem->dirname($final_destination); + if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) { + throw new MigrateException("Could not create or write to directory '$dir'"); + } + // Let's try that fopen again. + $destination_stream = @fopen($final_destination, 'w'); + if (!$destination_stream) { + throw new MigrateException("Could not write to file '$final_destination'"); + } + } + + // Stream the request body directly to the final destination stream. + $this->configuration['guzzle_options']['sink'] = $destination_stream; + + // Make the request. Guzzle throws an exception for anything other than 200. + $this->httpClient->get($source, $this->configuration['guzzle_options']); + + return $final_destination; + } + +} diff --git a/core/modules/migrate/tests/src/Unit/process/DownloadTest.php b/core/modules/migrate/tests/src/Unit/process/DownloadTest.php new file mode 100644 index 0000000..e4de42e --- /dev/null +++ b/core/modules/migrate/tests/src/Unit/process/DownloadTest.php @@ -0,0 +1,128 @@ +container->get('stream_wrapper_manager')->registerWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream', StreamWrapperInterface::LOCAL_NORMAL); + } + + /** + * Tests a download that overwrites an existing local file. + */ + public function testOverwritingDownload() { + // Create a pre-existing file at the destination, to test overwrite behavior. + $destination_uri = $this->createUri('existing_file.txt'); + + // Test destructive download. + $actual_destination = $this->doTransform($destination_uri); + $this->assertSame($destination_uri, $actual_destination, 'Import returned a destination that was not renamed'); + $this->assertFileNotExists('public://existing_file_0.txt', 'Import did not rename the file'); + } + + /** + * Tests a download that renames the downloaded file if there's a collision. + */ + public function testNonDestructiveDownload() { + // Create a pre-existing file at the destination, to test overwrite behavior. + $destination_uri = $this->createUri('another_existing_file.txt'); + + // Test non-destructive download. + $actual_destination = $this->doTransform($destination_uri, ['rename' => TRUE]); + $this->assertSame('public://another_existing_file_0.txt', $actual_destination, 'Import returned a renamed destination'); + $this->assertFileExists($actual_destination, 'Downloaded file was created'); + } + + /** + * Tests that an exception is thrown if the destination URI is not writable. + */ + public function testWriteProectedDestination() { + // Create a pre-existing file at the destination, to test overwrite behavior. + $destination_uri = $this->createUri('not-writable.txt'); + + // Make the destination non-writable. + $this->container + ->get('file_system') + ->chmod($destination_uri, 0444); + + // Pass or fail, we'll need to make the file writable again so the test + // can clean up after itself. + $fix_permissions = function () use ($destination_uri) { + $this->container + ->get('file_system') + ->chmod($destination_uri, 0755); + }; + + try { + $this->doTransform($destination_uri); + $fix_permissions(); + $this->fail('MigrateException was not thrown for non-writable destination URI.'); + } + catch (MigrateException $e) { + $this->assertTrue(TRUE, 'MigrateException was thrown for non-writable destination URI.'); + $fix_permissions(); + } + } + + /** + * Runs an input value through the download plugin. + * + * @param string $destination_uri + * The destination URI to download to. + * @param array $configuration + * (optional) Configuration for the download plugin. + * + * @return string + * The local URI of the downloaded file. + */ + protected function doTransform($destination_uri, $configuration = []) { + // The HTTP client will return a file with contents 'It worked!' + $body = fopen('data://text/plain;base64,SXQgd29ya2VkIQ==', 'r'); + + // Prepare a mock HTTP client. + $this->container->set('http_client', $this->getMock(Client::class)); + $this->container->get('http_client') + ->method('get') + ->willReturn(new Response(200, [], $body)); + + // Instantiate the plugin statically so it can pull dependencies out of + // the container. + $plugin = Download::create($this->container, $configuration, 'download', []); + + // Execute the transformation. + $executable = $this->getMock(MigrateExecutableInterface::class); + $row = new Row([], []); + + // Return the downloaded file's local URI. + $value = [ + 'http://drupal.org/favicon.ico', + $destination_uri, + ]; + return $plugin->transform($value, $executable, $row, 'foobaz'); + } + +}