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..2272c23
--- /dev/null
+++ b/core/modules/migrate/src/Plugin/migrate/process/Download.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Drupal\migrate\Plugin\migrate\process;
+
+use Drupal\Core\File\FileSystemInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\MigrateExecutableInterface;
+use Drupal\migrate\ProcessPluginBase;
+use Drupal\migrate\Row;
+use GuzzleHttp\Client;
+use GuzzleHttp\ClientInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Downloads a file from a remote location into the local file system.
+ *
+ * @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 download 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\File\FileSystemInterface $file_system
+   *   The file system service.
+   * @param \GuzzleHttp\ClientInterface $http_client
+   *   The HTTP client.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, FileSystemInterface $file_system, ClientInterface $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;
+
+    // $source must be a valid URL.
+    if (parse_url($source) === FALSE) {
+      throw new \InvalidArgumentException('Source URL is not valid');
+    }
+
+    // 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.
+    $this->httpClient->request('GET', $source, $this->configuration['guzzle_options']);
+
+    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..3b11ec2
--- /dev/null
+++ b/core/modules/migrate/tests/src/Unit/process/DownloadTest.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\Tests\migrate\Unit\process;
+
+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\ClientInterface;
+use GuzzleHttp\Psr7\Response;
+
+/**
+ * Tests the download process plugin.
+ *
+ * @group migrate
+ */
+class DownloadTest extends FileTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->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() {
+    // 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');
+
+    // Test destructive download.
+    $actual_destination = $this->doTransform($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');
+  }
+
+  /**
+   * Tests a download that renames the downloaded file if there's a collision.
+   */
+  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');
+
+    // Test non-destructive download.
+    $actual_destination = $this->doTransform($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');
+  }
+
+  /**
+   * Runs an input value through the download plugin.
+   *
+   * @param string $source_path
+   *   Source path to copy from.
+   * @param string $destination_path
+   *   The destination path to copy to.
+   * @param array $configuration
+   *   (optional) Configuration for the download plugin.
+   *
+   * @return string
+   *   The local URI of the downloaded file.
+   */
+  protected function doTransform($source_path, $destination_path, $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(ClientInterface::class));
+    $this->container->get('http_client')
+      ->method('request')
+      ->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.
+    return $plugin->transform([$source_path, $destination_path], $executable, $row, 'foobaz');
+  }
+
+}
