diff --git a/core/modules/migrate/tests/src/Unit/MigrateSqlSourceTestCase.php b/core/modules/migrate/tests/src/Unit/MigrateSqlSourceTestCase.php
index 16d7e0b..70c544a 100644
--- a/core/modules/migrate/tests/src/Unit/MigrateSqlSourceTestCase.php
+++ b/core/modules/migrate/tests/src/Unit/MigrateSqlSourceTestCase.php
@@ -76,11 +76,24 @@ protected function setUp() {
     $migration->expects($this->any())
       ->method('getHighWater')
       ->will($this->returnValue(static::ORIGINAL_HIGH_WATER));
-    // Need the test class, not the original because we need a setDatabase method. This is not pretty :/
-    $plugin_class  = preg_replace('/^Drupal\\\\(\w+)\\\\Plugin\\\\migrate(\\\\source(\\\\.+)?\\\\)([^\\\\]+)$/', 'Drupal\\Tests\\\$1\\Unit$2Test$4', static::PLUGIN_CLASS);
+
+    // Give each test the oppurtunity to provide a test version of the plugin
+    // we're testing.
+    $test_plugin_class = $plugin_class  = preg_replace('/^Drupal\\\\(\w+)\\\\Plugin\\\\migrate(\\\\source(\\\\.+)?\\\\)([^\\\\]+)$/', 'Drupal\\Tests\\\$1\\Unit$2Test$4', static::PLUGIN_CLASS);
+    $plugin_class = class_exists($test_plugin_class) ? $test_plugin_class : static::PLUGIN_CLASS;
     $plugin = new $plugin_class($this->migrationConfiguration['source'], $this->migrationConfiguration['source']['plugin'], array(), $migration, $entity_manager);
-    $plugin->setDatabase($this->getDatabase($this->databaseContents + array('test_map' => array())));
-    $plugin->setModuleHandler($module_handler);
+
+    // Do some reflection to set the database and moduleHandler.
+    $plugin_reflection = new \ReflectionClass($plugin);
+    $database_property = $plugin_reflection->getProperty('database');
+    $database_property->setAccessible(TRUE);
+    $module_handler_property = $plugin_reflection->getProperty('moduleHandler');
+    $module_handler_property->setAccessible(TRUE);
+
+    // Set the database and the module handler onto our plugin.
+    $database_property->setValue($plugin, $this->getDatabase($this->databaseContents + array('test_map' => array())));
+    $module_handler_property->setValue($plugin, $module_handler);
+
     $plugin->setStringTranslation($this->getStringTranslationStub());
     $migration->expects($this->any())
       ->method('getSourcePlugin')
diff --git a/core/modules/migrate_drupal/config/optional/migrate.migration.d7_file.yml b/core/modules/migrate_drupal/config/optional/migrate.migration.d7_file.yml
new file mode 100644
index 0000000..fed51f5
--- /dev/null
+++ b/core/modules/migrate_drupal/config/optional/migrate.migration.d7_file.yml
@@ -0,0 +1,29 @@
+# Every migration that references a file by fid should specify this migration
+# as an optional dependency.
+id: d7_file
+label: Drupal 7 files
+migration_tags:
+  - Drupal 7
+source:
+  plugin: d7_file
+process:
+  fid: fid
+  filename: filename
+  uri: uri
+  filemime: filemime
+  # filesize is dynamically computed when file entities are saved, so there is
+  # no point in migrating it.
+  # filesize: filesize
+  status: status
+  # Drupal 7 didn't keep track of the file's creation or update time -- all it
+  # had was the vague "timestamp" column. So we'll use it for both.
+  created: timestamp
+  changed: timestamp
+  uid: uid
+destination:
+  plugin: entity:file
+  source_path_property: filepath
+dependencies:
+  module:
+    - file
+    - migrate_drupal
diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/File.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/File.php
index ed307a4..4abbbae 100644
--- a/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/File.php
+++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/d6/File.php
@@ -58,7 +58,6 @@ public function query() {
     return $query;
   }
 
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/d7/File.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/d7/File.php
new file mode 100644
index 0000000..4958b74
--- /dev/null
+++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/d7/File.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate_drupal\Plugin\migrate\source\d7\File.
+ */
+
+namespace Drupal\migrate_drupal\Plugin\migrate\source\d7;
+
+use Drupal\migrate\Row;
+use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
+
+/**
+ * Drupal 7 file source from database.
+ *
+ * @MigrateSource(
+ *   id = "d7_file"
+ * )
+ */
+class File extends DrupalSqlBase {
+
+  /**
+   * The public file directory path.
+   *
+   * @var string
+   */
+  protected $publicPath;
+
+  /**
+   * The private file directory path, if any.
+   *
+   * @var string
+   */
+  protected $privatePath;
+
+  /**
+   * The temporary file directory path.
+   *
+   * @var string
+   */
+  protected $temporaryPath;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    return $this->select('file_managed', 'f')
+      ->fields('f')
+      ->orderBy('timestamp');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function initializeIterator() {
+    $this->publicPath = $this->variableGet('file_public_path', 'sites/default/files');
+    $this->privatePath = $this->variableGet('file_private_path', NULL);
+    $this->temporaryPath = $this->variableGet('file_temporary_path', '/tmp');
+    return parent::initializeIterator();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareRow(Row $row) {
+    // Compute the filepath property, which is a physical representation of
+    // the URI relative to the Drupal root.
+    $path = str_replace(['public:/', 'private:/', 'temporary:/'], [$this->publicPath, $this->privatePath, $this->temporaryPath], $row->getSourceProperty('uri'));
+    // At this point, $path could be an absolute path or a relative path,
+    // depending on how the scheme's variable was set. So we need to shear out
+    // the source_base_path in order to make them all relative.
+    $path = str_replace($this->migration->get('destination.source_base_path'), NULL, $path);
+    $row->setSourceProperty('filepath', $path);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fields() {
+    return array(
+      'fid' => $this->t('File ID'),
+      'uid' => $this->t('The {users}.uid who added the file. If set to 0, this file was added by an anonymous user.'),
+      'filename' => $this->t('File name'),
+      'filepath' => $this->t('File path'),
+      'filemime' => $this->t('File Mime Type'),
+      'status' => $this->t('The published status of a file.'),
+      'timestamp' => $this->t('The time that the file was added.'),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIds() {
+    $ids['fid']['type'] = 'integer';
+    return $ids;
+  }
+
+}
diff --git a/core/modules/migrate_drupal/src/Tests/d7/MigrateFileTest.php b/core/modules/migrate_drupal/src/Tests/d7/MigrateFileTest.php
new file mode 100644
index 0000000..a264175
--- /dev/null
+++ b/core/modules/migrate_drupal/src/Tests/d7/MigrateFileTest.php
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate_drupal\Tests\d7\MigrateFileTest.
+ */
+
+namespace Drupal\migrate_drupal\Tests\d7;
+
+use Drupal\Core\Database\Database;
+use Drupal\file\Entity\File;
+use Drupal\file\FileInterface;
+use Drupal\migrate\MigrateExecutable;
+
+/**
+ * Migrates all files in the file_managed table.
+ *
+ * @group migrate_drupal 7.x
+ */
+class MigrateFileTest extends MigrateDrupal7TestBase {
+
+  static $modules = array('file');
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->loadDumps([
+      $this->getDumpDirectory() . '/FileManaged.php',
+      $this->getDumpDirectory() . '/Variable.php',
+    ]);
+    $this->installEntitySchema('file');
+
+    $fs = \Drupal::service('file_system');
+    // The public file directory active during the test will serve as the
+    // root of the fictional Drupal 7 site we're migrating.
+    $fs->mkdir('public://sites/default/files', NULL, TRUE);
+    file_put_contents('public://sites/default/files/cube.jpeg', str_repeat('*', 3620));
+
+    /** @var \Drupal\migrate\Entity\MigrationInterface $migration */
+    $migration = entity_load('migration', 'd7_file');
+    // Set the destination plugin's source_base_path configuration value, which
+    // would normally be set by the user running the migration.
+    $migration->set('destination', [
+      'plugin' => 'entity:file',
+      // Note that source_base_path must include a trailing slash because it's
+      // prepended directly to the value of the source path property.
+      'source_base_path' => $fs->realpath($this->publicFilesDirectory) . '/',
+      // This is set in the migration's YAML file, but we need to repeat it
+      // here because all the destination configuration must be set at once.
+      'source_path_property' => 'filepath',
+    ]);
+    $executable = new MigrateExecutable($migration, $this);
+    $executable->import();
+  }
+
+  /**
+   * Tests a single file entity.
+   *
+   * @param integer $id
+   *  The file ID.
+   * @param string $name
+   *  The expected file name.
+   * @param string $uri
+   *  The expected URI.
+   * @param string $mime
+   *  The expected MIME type.
+   * @param integer $size
+   *  The expected file size.
+   * @param integer $created
+   *  The expected creation time.
+   * @param integer $changed
+   *  The expected modification time.
+   * @param integer $uid
+   *  The expected owner ID.
+   */
+  protected function assertEntity($id, $name, $uri, $mime, $size, $created, $changed, $uid) {
+    /** @var \Drupal\file\FileInterface $file */
+    $file = File::load($id);
+    $this->assertTrue($file instanceof FileInterface);
+    $this->assertIdentical($name, $file->getFilename());
+    $this->assertIdentical($uri, $file->getFileUri());
+    $this->assertTrue(file_exists($uri));
+    $this->assertIdentical($mime, $file->getMimeType());
+    $this->assertIdentical($size, $file->getSize());
+    // isPermanent(), isTemporary(), etc. are determined by the status column.
+    $this->assertTrue($file->isPermanent());
+    $this->assertIdentical($created, $file->getCreatedTime());
+    $this->assertIdentical($changed, $file->getChangedTime());
+    $this->assertIdentical($uid, $file->getOwnerId());
+  }
+
+  /**
+   * Tests that all expected files are migrated.
+   */
+  public function testFileMigration() {
+    $this->assertEntity(1, 'cube.jpeg', 'public://cube.jpeg', 'image/jpeg', '3620', '1421727515', '1421727515', '1');
+  }
+
+}
diff --git a/core/modules/migrate_drupal/tests/src/Unit/source/d7/FileTest.php b/core/modules/migrate_drupal/tests/src/Unit/source/d7/FileTest.php
new file mode 100644
index 0000000..66510ab
--- /dev/null
+++ b/core/modules/migrate_drupal/tests/src/Unit/source/d7/FileTest.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\migrate_drupal\Unit\source\d7\FileTest.
+ */
+
+namespace Drupal\Tests\migrate_drupal\Unit\source\d7;
+
+use Drupal\Tests\migrate\Unit\MigrateSqlSourceTestCase;
+
+/**
+ * Tests D7 file source plugin.
+ *
+ * @group migrate_drupal
+ */
+class FileTest extends MigrateSqlSourceTestCase {
+
+  const PLUGIN_CLASS = 'Drupal\migrate_drupal\Plugin\migrate\source\d7\File';
+
+  // The fake Migration configuration entity.
+  protected $migrationConfiguration = array(
+    // The ID of the entity, can be any string.
+    'id' => 'test',
+    // Leave it empty for now.
+    'idlist' => array(),
+    'source' => array(
+      'plugin' => 'd7_file',
+    ),
+  );
+
+  protected $expectedResults = [
+    [
+      'fid' => '1',
+      'uid' => '1',
+      'filename' => 'cube.jpeg',
+      'uri' => 'public://cube.jpeg',
+      'filemime' => 'image/jpeg',
+      'filesize' => '3620',
+      'status' => '1',
+      'timestamp' => '1421727515',
+    ],
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $this->databaseContents['file_managed'] = $this->expectedResults;
+    parent::setUp();
+  }
+
+}
