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..f030bd0
--- /dev/null
+++ b/core/modules/migrate/src/Plugin/migrate/process/Download.php
@@ -0,0 +1,183 @@
+<?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 stream wrapper manager service.
+   *
+   * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
+   */
+  protected $streamWrapperManager;
+
+  /**
+   * 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, StreamWrapperManagerInterface $stream_wrappers, FileSystemInterface $file_system, Client $http_client) {
+    $configuration += array(
+      'rename' => FALSE,
+      'guzzle_options' => array(),
+    );
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->streamWrapperManager = $stream_wrappers;
+    $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('stream_wrapper_manager'),
+      $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;
+
+    $replace = $this->getOverwriteMode();
+
+    // Make the request. Guzzle throws an exception for anything other than 200.
+    try {
+      $response = $this->httpClient->request('GET', $source, $this->configuration['guzzle_options']);
+    }
+    catch (RequestException $e) {
+      throw new MigrateException("Could not download remote file $source. Request failed with $e->getCode(): $e->getMessage()");
+    }
+
+    // Try saving the file first, to avoid calling file_prepare_directory() unnecessarily.
+    $final_destination = $this->writeFile($response, $destination, $replace);
+    if ($final_destination) {
+      return $final_destination;
+    }
+
+    // If writeFile 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'");
+    }
+    $final_destination = $this->writeFile($response, $destination, $replace);
+    if ($final_destination) {
+      return $final_destination;
+    }
+    throw new MigrateException("File $source could not be saved to $destination");
+  }
+
+  /**
+   * Tries to move or copy a file.
+   *
+   * @param Response $response
+   *   The PSR-7 Response for the source file.
+   * @param string $destination
+   *   The destination path or URI.
+   * @param int $replace
+   *   (optional) FILE_EXISTS_REPLACE (default) or FILE_EXISTS_RENAME.
+   *
+   * @return string|bool
+   *   File destination on success, FALSE on failure.
+   */
+  protected function writeFile($response, $destination, $replace = FILE_EXISTS_REPLACE) {
+    $final_destination = file_destination($destination, $replace);
+    if(@file_put_contents($final_destination, $response->getBody())) {
+      return $final_destination;
+    }
+    return FALSE;
+  }
+
+  /**
+   * 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..7b64792
--- /dev/null
+++ b/core/modules/migrate/tests/src/Unit/process/DownloadTest.php
@@ -0,0 +1,110 @@
+<?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;
+
+  /**
+   * @var Client
+   */
+  protected $httpClient;
+
+  /**
+   * @var String
+   */
+  protected $human_readable_body_contents;
+
+
+  /**
+   * {@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);
+    $this->httpClient = $this->getMockBuilder('GuzzleHttp\Client')->getMock();
+
+    // Set up the HTTPClient mock with a known file.
+    $body = fopen('data://text/plain;base64,RG93bmxvYWQgdGVzdCBzdWNjZXNzZnVs', 'r');
+    $this->human_readable_body_contents = 'Download test successful';
+    $this->httpClient->expects($this->any())->method('get')
+      ->willReturn(new Response(200, array(), $body));
+  }
+
+  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, array('rename' => TRUE));
+    $this->assertSame($destination_path, $actual_destination, 'Import returned a destination that was not renamed');
+    $file_contents = file_get_contents($destination_path);
+    $this->assertSame($this->human_readable_body_contents, $file_contents, 'Original file was overwritten with downloaded contents');
+  }
+
+  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');
+    $original_file_contents = file_get_contents($destination_path);
+    $downloaded_file_contents = file_get_contents($actual_destination);
+    $this->assertNotSame($this->human_readable_body_contents, $original_file_contents, 'Original file was not overwritten');
+    $this->assertSame($this->human_readable_body_contents, $downloaded_file_contents, 'File contents were saved');
+  }
+
+  /**
+   * 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 = []) {
+    $plugin = Download::create($this->container, $configuration, 'download', []);
+    $executable = $this->prophesize(MigrateExecutableInterface::class)->reveal();
+    $row = new Row([], []);
+
+    $result = $plugin->transform([$source_path, $destination_path], $executable, $row, 'foobaz');
+
+    // Return the imported file Uri.
+    return $result;
+  }
+}
