diff --git a/core/modules/media_entity/config/schema/media_entity.schema.yml b/core/modules/media_entity/config/schema/media_entity.schema.yml
index bca1a93eb8b5b748ebfed3bed8556f8fd2488a44..68f5962fb21b39ca2705e29f3cb40aafd7a8a290 100644
--- a/core/modules/media_entity/config/schema/media_entity.schema.yml
+++ b/core/modules/media_entity/config/schema/media_entity.schema.yml
@@ -36,6 +36,14 @@ media_entity.bundle.*:
       sequence:
         type: string
 
+media_entity.bundle.type.file:
+  type: mapping
+  label: 'File handler configuration'
+  mapping:
+    source_field:
+      type: string
+      label: 'Field with source information'
+
 action.configuration.media_delete_action:
   type: action_configuration_default
   label: 'Delete media configuration'
diff --git a/core/modules/media_entity/src/Plugin/MediaEntity/Type/File.php b/core/modules/media_entity/src/Plugin/MediaEntity/Type/File.php
new file mode 100644
index 0000000000000000000000000000000000000000..8933daa2258e7e1d365cd6a4740df06ef226c7a9
--- /dev/null
+++ b/core/modules/media_entity/src/Plugin/MediaEntity/Type/File.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\media_entity\Plugin\MediaEntity\Type;
+
+use Drupal\media_entity\MediaBundleInterface;
+use Drupal\media_entity\MediaInterface;
+use Drupal\media_entity\MediaTypeBase;
+
+/**
+ * Provides the media handler plugin for Files.
+ *
+ * @MediaType(
+ *   id = "file",
+ *   label = @Translation("File"),
+ *   description = @Translation("Provides business logic and metadata for local files and documents."),
+ *   allowed_field_types = {"file"},
+ * )
+ */
+class File extends MediaTypeBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function providedFields() {
+    return [
+      'mime' => $this->t('MIME type'),
+      'size' => $this->t('Size'),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getField(MediaInterface $media, $name) {
+    $source_field = $this->configuration['source_field'];
+
+    // Get the file.
+    /** @var \Drupal\file\FileInterface $file */
+    $file = $media->{$source_field}->entity;
+
+    // Return the field.
+    switch ($name) {
+      case 'mime':
+        return $file->getMimeType() ?: FALSE;
+
+      case 'size':
+        $size = $file->getSize();
+        return is_numeric($size) ? $size : FALSE;
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function thumbnail(MediaInterface $media) {
+    $source_field = $this->configuration['source_field'];
+    /** @var \Drupal\file\FileInterface $file */
+    $file = $media->{$source_field}->entity;
+    $icon_base = $this->configFactory->get('media_entity.settings')->get('icon_base');
+    $thumbnail = FALSE;
+    if ($file) {
+      $mimetype = $file->getMimeType();
+      $mimetype = explode('/', $mimetype);
+      $thumbnail = $icon_base . "/{$mimetype[0]}--{$mimetype[1]}.png";
+
+      if (!is_file($thumbnail)) {
+        $thumbnail = $icon_base . "/{$mimetype[1]}.png";
+      }
+    }
+
+    if (!is_file($thumbnail)) {
+      $thumbnail = $icon_base . '/generic.png';
+    }
+
+    return $thumbnail;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultName(MediaInterface $media) {
+    // The default name will be the filename of the source_field, if present.
+    $source_field = $this->configuration['source_field'];
+
+    /** @var \Drupal\file\FileInterface $file */
+    if (!empty($source_field) && ($file = $media->{$source_field}->entity)) {
+      return $file->getFilename();
+    }
+
+    return parent::getDefaultName($media);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createSourceFieldStorage() {
+    return $this->entityTypeManager
+      ->getStorage('field_storage_config')
+      ->create([
+        'entity_type' => 'media',
+        'field_name' => $this->getSourceFieldName(),
+        'type' => 'file',
+      ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createSourceField(MediaBundleInterface $bundle) {
+    /** @var \Drupal\field\FieldConfigInterface $field */
+    return $this->entityTypeManager
+      ->getStorage('field_config')
+      ->create([
+        'field_storage' => $this->getSourceFieldStorage(),
+        'bundle' => $bundle->id(),
+      ]);
+  }
+
+}
diff --git a/core/modules/media_entity/tests/src/FunctionalJavascript/FileTest.php b/core/modules/media_entity/tests/src/FunctionalJavascript/FileTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..51af1a73bf92a3794d40db49a7dcf51f71342012
--- /dev/null
+++ b/core/modules/media_entity/tests/src/FunctionalJavascript/FileTest.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\Tests\media_entity\FunctionalJavascript;
+
+use Drupal\media_entity\Entity\Media;
+
+/**
+ * Tests the file handler plugin.
+ *
+ * @group media_entity
+ */
+class FileTest extends MediaHandlerTestBase {
+
+  /**
+   * Tests the file handler.
+   */
+  public function testFileHandler() {
+    $session = $this->getSession();
+    $page = $session->getPage();
+    $assert_session = $this->assertSession();
+
+    // We rely on an automatic source field being created at this point.
+    // @see MediaTypeBase::getSourceFieldName().
+    $source_field_name = 'field_media_file';
+    $bundle_name = strtolower($this->randomMachineName(12));
+    $provided_fields = ['mime', 'size'];
+    $bundle = $this->createMediaBundleTest($bundle_name, 'file', $provided_fields);
+    // Adjust the allowed extensions on the source field.
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+    $bundle->getType()
+      ->getSourceField($bundle)
+      ->setSetting('file_extensions', 'txt')
+      ->save();
+    // Hide the media name to test default name generation.
+    $this->hideMediaField('name', $bundle_name);
+    // Create a media item.
+    $this->drupalGet("media/add/$bundle_name");
+    $page->attachFileToField('files[' . $source_field_name . '_0]', \Drupal::root() . '/sites/README.txt');
+    $assert_session->assertWaitOnAjaxRequest();
+    $page->pressButton('Save and publish');
+
+    $assert_session->addressEquals('media/1');
+    // Make sure the thumbnail shows up.
+    $assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'src', 'generic.png');
+    // Load the media and check its default name.
+    $media = Media::load(1);
+    $this->assertEquals($media->label(), 'README.txt');
+
+  }
+
+}
diff --git a/core/modules/media_entity/tests/src/FunctionalJavascript/MediaHandlerTestBase.php b/core/modules/media_entity/tests/src/FunctionalJavascript/MediaHandlerTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..0f6a79d90ed2860a4d23b21fb57013f60b8bb132
--- /dev/null
+++ b/core/modules/media_entity/tests/src/FunctionalJavascript/MediaHandlerTestBase.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\Tests\media_entity\FunctionalJavascript;
+
+use Drupal\media_entity\Entity\MediaBundle;
+
+/**
+ * A base test class for plugin types.
+ */
+abstract class MediaHandlerTestBase extends MediaEntityJavascriptTestBase {
+
+  /**
+   * Hide a component from the default form display config.
+   *
+   * @param string $field_name
+   *   The field name.
+   * @param string $bundle_name
+   *   The media bundle machine name.
+   */
+  protected function hideMediaField($field_name, $bundle_name) {
+    $form_display = entity_get_form_display('media', $bundle_name, 'default');
+    $form_display->removeComponent($field_name)->save();
+  }
+
+  /**
+   * Helper to test a generic bundle creation.
+   *
+   * @param string $bundle_name
+   *   The bundle machine name.
+   * @param string $bundle_type
+   *   The bundle type ID.
+   * @param array $provided_fields
+   *   (optional) An array of field machine names this type provides.
+   *
+   * @return \Drupal\media_entity\MediaBundleInterface
+   *   The bundle created.
+   */
+  public function createMediaBundleTest($bundle_name, $bundle_type, array $provided_fields = []) {
+    $session = $this->getSession();
+    $page = $session->getPage();
+    $assert_session = $this->assertSession();
+
+    $this->drupalGet('admin/structure/media/add');
+    $page->fillField('label', $bundle_name);
+    // assertWaitOnAjaxRequest() doesn't work on the machine name element.
+    $session->wait(5000, "jQuery('.machine-name-value').text() === '$bundle_name'");
+
+    // Make sure the bundle type is available as plugin type.
+    $assert_session->optionExists('type', $bundle_type);
+    $page->selectFieldOption('type', $bundle_type);
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Make sure the provided fields are visible on the form.
+    if (!empty($provided_fields)) {
+      foreach ($provided_fields as $provided_field) {
+        $assert_session->selectExists("field_mapping[$provided_field]");
+      }
+    }
+
+    // Save the page to create the bundle.
+    $page->pressButton('Save');
+    $assert_session->pageTextContains('The media type ' . $bundle_name . ' has been added.');
+    $this->drupalGet('admin/structure/media');
+    $assert_session->pageTextContains($bundle_name);
+
+    // Bundle definitions are statically cached in the context of the test, we
+    // need to make sure we have updated information before proceeding with the
+    // actions on the UI.
+    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+
+    return MediaBundle::load($bundle_name);
+  }
+
+  /**
+   * Helper to assert presence/absence of select options.
+   *
+   * @param string $select
+   *   One of id|name|label|value for the select field.
+   * @param array $expected_options
+   *   An indexed array of expected option names.
+   * @param array $non_expected_options
+   *   An indexed array of non-expected option names.
+   *
+   * @see \Drupal\Tests\WebAssert::optionExists()
+   * @see \Drupal\Tests\WebAssert::optionNotExists()
+   */
+  protected function assertSelectOptions($select, $expected_options = [], $non_expected_options = []) {
+    $assert_session = $this->assertSession();
+    foreach ($expected_options as $expected_option) {
+      $assert_session->optionExists($select, $expected_option);
+    }
+    foreach ($non_expected_options as $non_expected_option) {
+      $assert_session->optionNotExists($select, $non_expected_option);
+    }
+  }
+
+}
diff --git a/core/profiles/standard/config/optional/media_entity.bundle.file.yml b/core/profiles/standard/config/optional/media_entity.bundle.file.yml
new file mode 100644
index 0000000000000000000000000000000000000000..acdf44e1c24f29001215c4e68a353a3e3f412148
--- /dev/null
+++ b/core/profiles/standard/config/optional/media_entity.bundle.file.yml
@@ -0,0 +1,14 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - media_entity
+id: file
+label: File
+description: 'Use the "File" media type for uploading local files.'
+type: file
+queue_thumbnail_downloads: false
+new_revision: false
+type_configuration:
+  source_field: field_media_file
+field_map: { }
