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..c9b1050
--- /dev/null
+++ b/core/modules/migrate/src/Plugin/migrate/process/Download.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace Drupal\migrate\Plugin\migrate\process;
+
+use Drupal\Core\File\FileSystemInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\MigrateExecutableInterface;
+use Drupal\migrate\ProcessPluginBase;
+use Drupal\migrate\Row;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Psr7\Response;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Copy a file from one place into another.
+ *
+ * @MigrateProcessPlugin(
+ *   id = "download"
+ * )
+ */
+class Download extends ProcessPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The file system service.
+   *
+   * @var \Drupal\Core\File\FileSystemInterface
+   */
+  protected $fileSystem;
+
+  /**
+   * The Guzzle HTTP Client service.
+   *
+   * @var Client
+   */
+  protected $httpClient;
+
+  /**
+   * Constructs a file_copy process plugin.
+   *
+   * @param array $configuration
+   *   The plugin configuration.
+   * @param string $plugin_id
+   *   The plugin ID.
+   * @param mixed $plugin_definition
+   *   The plugin definition.
+   * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrappers
+   *   The stream wrapper manager service.
+   * @param \Drupal\Core\File\FileSystemInterface $file_system
+   *   The file system service.
+   * @param \GuzzleHttp\Client $http_client
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, FileSystemInterface $file_system, Client $http_client) {
+    $configuration += array(
+      'rename' => FALSE,
+      'guzzle_options' => array(),
+    );
+    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 = $this->getOverwriteMode();
+    $final_destination = file_destination($destination, $replace);
+
+    // Try opening the file first, to avoid calling file_prepare_directory() unnecessarily.
+    $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->getDirectory($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.
+    try {
+      $this->httpClient->request('GET', $source, $this->configuration['guzzle_options']);
+    }
+    catch (RequestException $e) {
+      $response = $e->getResponse();
+      throw new MigrateException("Could not download remote file $source. Request failed with $response->getStatusCode(): $response->getReasonPhrase()");
+    }
+
+    return $final_destination;
+  }
+
+  /**
+   * Determines how to handle file conflicts.
+   *
+   * @return int
+   *   Either FILE_EXISTS_REPLACE (default) or FILE_EXISTS_RENAME, depending
+   *   on the current configuration.
+   */
+  protected function getOverwriteMode() {
+    if (!empty($this->configuration['rename'])) {
+      return FILE_EXISTS_RENAME;
+    }
+    return FILE_EXISTS_REPLACE;
+  }
+
+  /**
+   * Returns the directory component of a URI or path.
+   *
+   * For URIs like public://foo.txt, the full physical path of public://
+   * will be returned, since a scheme by itself will trip up certain file
+   * API functions (such as file_prepare_directory()).
+   *
+   * @param string $uri
+   *   The URI or path.
+   *
+   * @return string|false
+   *   The directory component of the path or URI, or FALSE if it could not
+   *   be determined.
+   */
+  protected function getDirectory($uri) {
+    $dir = $this->fileSystem->dirname($uri);
+    if (substr($dir, -3) == '://') {
+      return $this->fileSystem->realpath($dir);
+    }
+    return $dir;
+  }
+
+}
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..1fa54f2
--- /dev/null
+++ b/core/modules/migrate/tests/src/Unit/process/DownloadTest.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\Tests\migrate\Unit\process;
+
+use Drupal\Core\File\FileSystemInterface;
+use Drupal\Core\StreamWrapper\StreamWrapperInterface;
+use Drupal\KernelTests\Core\File\FileTestBase;
+use Drupal\migrate\Plugin\migrate\process\Download;
+use Drupal\migrate\MigrateExecutableInterface;
+use Drupal\migrate\Row;
+use GuzzleHttp\Client;
+use GuzzleHttp\Psr7\Response;
+
+/**
+ * Tests the download process plugin.
+ *
+ * @group migrate
+ */
+class DownloadTest extends FileTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system'];
+
+  /**
+   * @var FileSystemInterface
+   */
+  protected $fileSystem;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->fileSystem = $this->container->get('file_system');
+    $this->container->get('stream_wrapper_manager')->registerWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream', StreamWrapperInterface::LOCAL_NORMAL);
+  }
+
+  public function testOverwritingDownload() {
+
+    // Source won't matter, the mock will always return the same anyway.
+    $source_path = 'http://drupal.org/favicon.ico';
+    // Create a pre-existing file at the destination, to test overwrite behavior.
+    $destination_path = $this->createUri('existing_file.txt', NULL, 'public');
+
+    // Test destructive download.
+    $actual_destination = $this->doImport($source_path, $destination_path);
+    $this->assertSame($destination_path, $actual_destination, 'Import returned a destination that was not renamed');
+    $this->assertFileNotExists('public://existing_file_0.txt', 'Import did not rename the file');
+  }
+
+  public function testNonDestructiveDownload() {
+    // Source won't matter, the mock will always return the same anyway.
+    $source_path = 'http://drupal.org/favicon.ico';
+    // Create a pre-existing file at the destination, to test overwrite behavior.
+    $destination_path = $this->createUri('another_existing_file.txt', NULL, 'public');
+
+    // Test non-destructive download.
+    $actual_destination = $this->doImport($source_path, $destination_path, array('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');
+  }
+
+  /**
+   * Do an import using the destination.
+   *
+   * @param string $source_path
+   *   Source path to copy from.
+   * @param string $destination_path
+   *   The destination path to copy to.
+   * @param array $configuration
+   *   Process plugin configuration settings.
+   *
+   * @throws \Drupal\migrate\MigrateException
+   */
+  protected function doImport($source_path, $destination_path, $configuration = []) {
+    // Build an http_client mock.
+    $http_client = $this->getMock(Client::class);
+    // http_client will return a file with contents 'Download test successful'.
+    $body = fopen('data://text/plain;base64,RG93bmxvYWQgdGVzdCBzdWNjZXNzZnVs', 'r');
+    $http_client->expects($this->any())->method('request')
+      ->willReturn(new Response(200, array(), $body));
+
+    // Instantiate the Plugin with our mocked http_client.
+    $plugin = new Download($configuration, 'download', array(), $this->fileSystem, $http_client);
+
+    // Execute the transformation.
+    $executable = $this->getMock(MigrateExecutableInterface::class);
+    $row = new Row([], []);
+
+    $result = $plugin->transform([$source_path, $destination_path], $executable, $row, 'foobaz');
+
+    // Return the imported file Uri.
+    return $result;
+  }
+}
