 core/modules/file/file.services.yml                |   4 +
 core/modules/file/src/FileFieldUploader.php        | 488 +++++++++++++++++++++
 .../Plugin/rest/resource/FileUploadResource.php    | 323 ++------------
 .../src/Functional/FileUploadResourceTestBase.php  |  10 +-
 4 files changed, 530 insertions(+), 295 deletions(-)

diff --git a/core/modules/file/file.services.yml b/core/modules/file/file.services.yml
index 03eb7bccf7..472c196005 100644
--- a/core/modules/file/file.services.yml
+++ b/core/modules/file/file.services.yml
@@ -4,3 +4,7 @@ services:
     arguments: ['@database', 'file_usage', '@config.factory']
     tags:
       - { name: backend_overridable }
+
+  file.uploader.field:
+    class: Drupal\file\FileFieldUploader
+    arguments: ['@entity_type.manager', '@logger.channel.file', '@file_system', '@file.mime_type.guesser', '@token', '@lock', '@config.factory']
diff --git a/core/modules/file/src/FileFieldUploader.php b/core/modules/file/src/FileFieldUploader.php
new file mode 100644
index 0000000000..325e932d0d
--- /dev/null
+++ b/core/modules/file/src/FileFieldUploader.php
@@ -0,0 +1,488 @@
+<?php
+
+namespace Drupal\file;
+
+use Drupal\Component\Utility\Bytes;
+use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Validation\DrupalTranslator;
+use Drupal\Core\File\FileSystemInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Lock\LockBackendInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Utility\Token;
+use Drupal\Component\Render\PlainTextOutput;
+use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+use Symfony\Component\Validator\ConstraintViolation;
+
+/**
+ * Reads data from an upload stream and creates a corresponding file entity.
+ *
+ * This is implemented at the field level for the following reasons:
+ *   - Validation for uploaded files is tied to fields (allowed extensions, max
+ *     size, etc..).
+ *   - The actual files do not need to be stored in another temporary location,
+ *     to be later moved when they are referenced from a file field.
+ *   - Permission to upload a file can be determined by a user's field- and
+ *     entity-level access.
+ *
+ * @internal
+ */
+class FileFieldUploader {
+
+  /**
+   * The regex used to extract the filename from the content disposition header.
+   *
+   * @var string
+   */
+  const REQUEST_HEADER_FILENAME_REGEX = '@\bfilename(?<star>\*?)=\"(?<filename>.+)\"@';
+
+  /**
+   * The amount of bytes to read in each iteration when streaming file data.
+   *
+   * @var int
+   */
+  const BYTES_TO_READ = 8192;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * A logger instance.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * The file system service.
+   *
+   * @var \Drupal\Core\File\FileSystemInterface
+   */
+  protected $fileSystem;
+
+  /**
+   * The MIME type guesser.
+   *
+   * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface
+   */
+  protected $mimeTypeGuesser;
+
+  /**
+   * The token replacement instance.
+   *
+   * @var \Drupal\Core\Utility\Token
+   */
+  protected $token;
+
+  /**
+   * The lock service.
+   *
+   * @var \Drupal\Core\Lock\LockBackendInterface
+   */
+  protected $lock;
+
+  /**
+   * System file configuration.
+   *
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  protected $systemFileConfig;
+
+  /**
+   * Constructs a FileUploadResource instance.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   A logger instance.
+   * @param \Drupal\Core\File\FileSystemInterface $file_system
+   *   The file system service.
+   * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser
+   *   The MIME type guesser.
+   * @param \Drupal\Core\Utility\Token $token
+   *   The token replacement instance.
+   * @param \Drupal\Core\Lock\LockBackendInterface $lock
+   *   The lock service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, LoggerInterface $logger, FileSystemInterface $file_system, MimeTypeGuesserInterface $mime_type_guesser, Token $token, LockBackendInterface $lock, ConfigFactoryInterface $config_factory) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->logger = $logger;
+    $this->fileSystem = $file_system;
+    $this->mimeTypeGuesser = $mime_type_guesser;
+    $this->token = $token;
+    $this->lock = $lock;
+    $this->systemFileConfig = $config_factory->get('system.file');
+  }
+
+  /**
+   * Creates and validates a file entity for a file field from a file stream.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition of the field for which the file is to be uploaded.
+   * @param string $filename
+   *   The name of the file.
+   * @param resource $stream
+   *   The upload stream.
+   * @param \Drupal\Core\Session\AccountInterface $owner
+   *   The owner of the file. Note, it is the responsibility of the caller to
+   *   enforce access.
+   *
+   * @return \Drupal\file\FileInterface|\Drupal\Core\Entity\EntityConstraintViolationListInterface
+   *   The newly uploaded file entity, or a list of validation constraint
+   *   violations
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
+   *   Thrown when temporary files cannot be written, a lock cannot be acquired,
+   *   or when temporary files cannot be moved to their new location.
+   */
+  public function handleFileUploadForField(FieldDefinitionInterface $field_definition, $filename, $stream, AccountInterface $owner) {
+    assert(is_a($field_definition->getClass(), FileFieldItemList::class, TRUE));
+    $destination = $this->getUploadLocation($field_definition->getSettings());
+
+    // Check the destination file path is writable.
+    if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
+      throw new HttpException(500, 'Destination file path is not writable');
+    }
+
+    $validators = $this->getUploadValidators($field_definition);
+
+    $prepared_filename = $this->prepareFilename($filename, $validators);
+
+    // Create the file.
+    $file_uri = "{$destination}/{$prepared_filename}";
+
+    $temp_file_path = $this->readStreamDataToFile($stream);
+
+    // This will take care of altering $file_uri if a file already exists.
+    file_unmanaged_prepare($temp_file_path, $file_uri);
+
+    // Lock based on the prepared file URI.
+    $lock_id = $this->generateLockIdFromFileUri($file_uri);
+
+    if (!$this->lock->acquire($lock_id)) {
+      throw new HttpException(503, sprintf('File "%s" is already locked for writing.'), NULL, ['Retry-After' => 1]);
+    }
+
+    // Begin building file entity.
+    $file = $this->entityTypeManager->getStorage('file')->create([]);
+    $file->setOwnerId($owner->id());
+    $file->setFilename($prepared_filename);
+    $file->setMimeType($this->mimeTypeGuesser->guess($prepared_filename));
+    $file->setFileUri($file_uri);
+    // Set the size. This is done in File::preSave() but we validate the file
+    // before it is saved.
+    $file->setSize(@filesize($temp_file_path));
+
+    // Validate the file entity against entity-level validation and field-level
+    // validators.
+    $violations = $this->validate($file, $validators);
+    if ($violations->count() > 0) {
+      return $violations;
+    }
+
+    // Move the file to the correct location after validation. Use
+    // FILE_EXISTS_ERROR as the file location has already been determined above
+    // in file_unmanaged_prepare().
+    if (!file_unmanaged_move($temp_file_path, $file_uri, FILE_EXISTS_ERROR)) {
+      throw new HttpException(500, 'Temporary file could not be moved to file location.');
+    }
+
+    $file->save();
+
+    $this->lock->release($lock_id);
+
+    return $file;
+  }
+
+  /**
+   * Validates and extracts the filename from the Content-Disposition header.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return string
+   *   The filename extracted from the header.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   *   Thrown when the 'Content-Disposition' request header is invalid.
+   */
+  public static function validateAndParseContentDispositionHeader(Request $request) {
+    // First, check the header exists.
+    if (!$request->headers->has('content-disposition')) {
+      throw new BadRequestHttpException('"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided.');
+    }
+
+    $content_disposition = $request->headers->get('content-disposition');
+
+    // Parse the header value. This regex does not allow an empty filename.
+    // i.e. 'filename=""'. This also matches on a word boundary so other keys
+    // like 'not_a_filename' don't work.
+    if (!preg_match(static::REQUEST_HEADER_FILENAME_REGEX, $content_disposition, $matches)) {
+      throw new BadRequestHttpException('No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided.');
+    }
+
+    // Check for the "filename*" format. This is currently unsupported.
+    if (!empty($matches['star'])) {
+      throw new BadRequestHttpException('The extended "filename*" format is currently not supported in the "Content-Disposition" header.');
+    }
+
+    // Don't validate the actual filename here, that will be done by the upload
+    // validators in validate().
+    // @see \Drupal\file\Plugin\rest\resource\FileUploadResource::validate()
+    $filename = $matches['filename'];
+
+    // Make sure only the filename component is returned. Path information is
+    // stripped as per https://tools.ietf.org/html/rfc6266#section-4.3.
+    return basename($filename);
+  }
+
+  /**
+   * Gets a standard file upload stream.
+   *
+   * @return resource
+   *   A stream ready to be read. Do not forget to close the stream after use
+   *   with fclose().
+   */
+  public static function getUploadStream() {
+    return fopen('php://input', 'rb');
+  }
+
+  /**
+   * Checks if the current user has access to upload the file.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The account for which file upload access should be checked.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition for which to get validators.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   (optional) The entity to which the file is to be uploaded, if it exists.
+   *   If the entity does not exist and it is not given, create access to the
+   *   file will be checked.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The file upload access result.
+   */
+  public static function checkFileUploadAccess(AccountInterface $account, FieldDefinitionInterface $field_definition, EntityInterface $entity = NULL) {
+    assert(is_null($entity) || $field_definition->getTargetEntityTypeId() === $entity->getEntityTypeId() && $field_definition->getTargetBundle() === $entity->bundle());
+    $entity_type_manager = \Drupal::entityTypeManager();
+    $entity_access_control_handler = $entity_type_manager->getAccessControlHandler($field_definition->getTargetEntityTypeId());
+    $bundle = $entity_type_manager->getDefinition($field_definition->getTargetEntityTypeId())->hasKey('bundle') ? $field_definition->getTargetBundle() : NULL;
+    $entity_access_result = $entity
+      ? $entity_access_control_handler->access($entity, 'update', $account, TRUE)
+      : $entity_access_control_handler->createAccess($bundle, $account, [], TRUE);
+    $field_access_result = $entity_access_control_handler->fieldAccess('edit', $field_definition, NULL, NULL, TRUE);
+    return $entity_access_result->andIf($field_access_result);
+  }
+
+  /**
+   * Reads file upload data to temporary file and moves to file destination.
+   *
+   * Note that the given stream will not be closed by this method.
+   *
+   * @param resource $stream
+   *   The stream on the file.
+   *
+   * @return string
+   *   The temp file path.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
+   *   Thrown when input data cannot be read, the temporary file cannot be
+   *   opened, or the temporary file cannot be written.
+   */
+  protected function readStreamDataToFile($stream) {
+    $temp_file_path = $this->fileSystem->tempnam('temporary://', 'file');
+    $temp_file = fopen($temp_file_path, 'wb');
+
+    if ($temp_file) {
+      while (!feof($stream)) {
+        $read = fread($stream, static::BYTES_TO_READ);
+
+        if ($read === FALSE) {
+          // Close the file streams.
+          fclose($temp_file);
+          $this->logger->error('Input data could not be read');
+          throw new HttpException(500, 'Input file data could not be read.');
+        }
+
+        if (fwrite($temp_file, $read) === FALSE) {
+          // Close the file streams.
+          fclose($temp_file);
+          $this->logger->error('Temporary file data for "%path" could not be written', ['%path' => $temp_file_path]);
+          throw new HttpException(500, 'Temporary file data could not be written.');
+        }
+      }
+
+      // Close the temp file stream.
+      fclose($temp_file);
+    }
+    else {
+      // Close the file streams.
+      fclose($temp_file);
+      $this->logger->error('Temporary file "%path" could not be opened for file upload.', ['%path' => $temp_file_path]);
+      throw new HttpException(500, 'Temporary file could not be opened');
+    }
+
+    return $temp_file_path;
+  }
+
+  /**
+   * Validates the file.
+   *
+   * @param \Drupal\file\FileInterface $file
+   *   The file entity to validate.
+   * @param array $validators
+   *   An array of upload validators to pass to file_validate().
+   *
+   * @return \Drupal\Core\Entity\EntityConstraintViolationListInterface
+   *   The list of constraint violations, if any.
+   */
+  protected function validate(FileInterface $file, array $validators) {
+    $violations = $file->validate();
+
+    // Remove violations of inaccessible fields as they cannot stem from our
+    // changes.
+    $violations->filterByFieldAccess();
+
+    // Validate the file based on the field definition configuration.
+    $errors = file_validate($file, $validators);
+    if (!empty($errors)) {
+      $translator = new DrupalTranslator();
+      foreach ($errors as $error) {
+        $violation = new ConstraintViolation($translator->trans($error),
+          $error,
+          [],
+          EntityAdapter::createFromEntity($file),
+          '',
+          NULL
+        );
+        $violations->add($violation);
+      }
+    }
+
+    return $violations;
+  }
+
+  /**
+   * Prepares the filename to strip out any malicious extensions.
+   *
+   * @param string $filename
+   *   The file name.
+   * @param array $validators
+   *   The array of upload validators.
+   *
+   * @return string
+   *   The prepared/munged filename.
+   */
+  protected function prepareFilename($filename, array &$validators) {
+    if (!empty($validators['file_validate_extensions'][0])) {
+      // If there is a file_validate_extensions validator and a list of
+      // valid extensions, munge the filename to protect against possible
+      // malicious extension hiding within an unknown file type. For example,
+      // "filename.html.foo".
+      $filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
+    }
+
+    // Rename potentially executable files, to help prevent exploits (i.e. will
+    // rename filename.php.foo and filename.php to filename.php.foo.txt and
+    // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
+    // evaluates to TRUE.
+    if (!$this->systemFileConfig->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename) && (substr($filename, -4) != '.txt')) {
+      // The destination filename will also later be used to create the URI.
+      $filename .= '.txt';
+
+      // The .txt extension may not be in the allowed list of extensions. We
+      // have to add it here or else the file upload will fail.
+      if (!empty($validators['file_validate_extensions'][0])) {
+        $validators['file_validate_extensions'][0] .= ' txt';
+      }
+    }
+
+    return $filename;
+  }
+
+  /**
+   * Determines the URI for a file field.
+   *
+   * @param array $settings
+   *   The array of field settings.
+   *
+   * @return string
+   *   An un-sanitized file directory URI with tokens replaced. The result of
+   *   the token replacement is then converted to plain text and returned.
+   */
+  protected function getUploadLocation(array $settings) {
+    $destination = trim($settings['file_directory'], '/');
+
+    // Replace tokens. As the tokens might contain HTML we convert it to plain
+    // text.
+    $destination = PlainTextOutput::renderFromHtml($this->token->replace($destination, [], [], new BubbleableMetadata()));
+    return $settings['uri_scheme'] . '://' . $destination;
+  }
+
+  /**
+   * Retrieves the upload validators for a field definition.
+   *
+   * This is copied from \Drupal\file\Plugin\Field\FieldType\FileItem as there
+   * is no entity instance available here that that a FileItem would exist for.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition for which to get validators.
+   *
+   * @return array
+   *   An array suitable for passing to file_save_upload() or the file field
+   *   element's '#upload_validators' property.
+   */
+  protected function getUploadValidators(FieldDefinitionInterface $field_definition) {
+    $validators = [
+      // Add in our check of the file name length.
+      'file_validate_name_length' => [],
+    ];
+    $settings = $field_definition->getSettings();
+
+    // Cap the upload size according to the PHP limit.
+    $max_filesize = Bytes::toInt(file_upload_max_size());
+    if (!empty($settings['max_filesize'])) {
+      $max_filesize = min($max_filesize, Bytes::toInt($settings['max_filesize']));
+    }
+
+    // There is always a file size limit due to the PHP server limit.
+    $validators['file_validate_size'] = [$max_filesize];
+
+    // Add the extension check if necessary.
+    if (!empty($settings['file_extensions'])) {
+      $validators['file_validate_extensions'] = [$settings['file_extensions']];
+    }
+
+    return $validators;
+  }
+
+  /**
+   * Generates a lock ID based on the file URI.
+   *
+   * @param string $file_uri
+   *   The file URI.
+   *
+   * @return string
+   *   The generated lock ID.
+   */
+  protected static function generateLockIdFromFileUri($file_uri) {
+    return 'file:rest:' . Crypt::hashBase64($file_uri);
+  }
+
+}
diff --git a/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
index 44600369df..dc4e592720 100644
--- a/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
+++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
@@ -2,33 +2,29 @@
 
 namespace Drupal\file\Plugin\rest\resource;
 
-use Drupal\Component\Utility\Bytes;
-use Drupal\Component\Utility\Crypt;
 use Drupal\Core\Config\Config;
+use Drupal\Core\Entity\EntityConstraintViolationListInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\Lock\LockBackendInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Utility\Token;
-use Drupal\file\FileInterface;
+use Drupal\file\FileFieldUploader;
 use Drupal\rest\ModifiedResourceResponse;
 use Drupal\rest\Plugin\ResourceBase;
 use Drupal\Component\Render\PlainTextOutput;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
-use Drupal\file\Entity\File;
 use Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait;
 use Drupal\rest\RequestHandler;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
-use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
 use Symfony\Component\Routing\Route;
 use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpKernel\Exception\HttpException;
+use Symfony\Component\Validator\ConstraintViolationInterface;
 
 /**
  * File upload resource.
@@ -124,6 +120,13 @@ class FileUploadResource extends ResourceBase {
    */
   protected $systemFileConfig;
 
+  /**
+   * The file field uploader.
+   *
+   * @var \Drupal\file\FileFieldUploader
+   */
+  protected $fileFieldUploader;
+
   /**
    * Constructs a FileUploadResource instance.
    *
@@ -153,8 +156,10 @@ class FileUploadResource extends ResourceBase {
    *   The lock service.
    * @param \Drupal\Core\Config\Config $system_file_config
    *   The system file configuration.
+   * @param \Drupal\file\FileFieldUploader $file_field_uploader
+   *   The file field uploader.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, $serializer_formats, LoggerInterface $logger, FileSystemInterface $file_system, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, AccountInterface $current_user, MimeTypeGuesserInterface $mime_type_guesser, Token $token, LockBackendInterface $lock, Config $system_file_config) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, $serializer_formats, LoggerInterface $logger, FileSystemInterface $file_system, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, AccountInterface $current_user, MimeTypeGuesserInterface $mime_type_guesser, Token $token, LockBackendInterface $lock, Config $system_file_config, FileFieldUploader $file_field_uploader = NULL) {
     parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
     $this->fileSystem = $file_system;
     $this->entityTypeManager = $entity_type_manager;
@@ -164,6 +169,13 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
     $this->token = $token;
     $this->lock = $lock;
     $this->systemFileConfig = $system_file_config;
+    if ($file_field_uploader instanceof FileFieldUploader) {
+      $this->fileFieldUploader = $file_field_uploader;
+    }
+    else {
+      @trigger_error("The 'file.uploader.field' service must be passed to FileUploadResource::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/project/drupal/issues/2940383.", E_USER_DEPRECATED);
+      $this->fileFieldUploader = \Drupal::service('file.uploader.field');
+    }
   }
 
   /**
@@ -183,7 +195,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $container->get('file.mime_type.guesser'),
       $container->get('token'),
       $container->get('lock'),
-      $container->get('config.factory')->get('system.file')
+      $container->get('config.factory')->get('system.file'),
+      $container->get('file.uploader.field')
     );
   }
 
@@ -219,164 +232,28 @@ public function permissions() {
    *   or when temporary files cannot be moved to their new location.
    */
   public function post(Request $request, $entity_type_id, $bundle, $field_name) {
-    $filename = $this->validateAndParseContentDispositionHeader($request);
-
     $field_definition = $this->validateAndLoadFieldDefinition($entity_type_id, $bundle, $field_name);
 
-    $destination = $this->getUploadLocation($field_definition->getSettings());
-
-    // Check the destination file path is writable.
-    if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
-      throw new HttpException(500, 'Destination file path is not writable');
-    }
-
-    $validators = $this->getUploadValidators($field_definition);
-
-    $prepared_filename = $this->prepareFilename($filename, $validators);
-
-    // Create the file.
-    $file_uri = "{$destination}/{$prepared_filename}";
-
-    $temp_file_path = $this->streamUploadData();
+    $filename = FileFieldUploader::validateAndParseContentDispositionHeader($request);
+    $stream = FileFieldUploader::getUploadStream();
+    $file = $this->fileFieldUploader->handleFileUploadForField($field_definition, $filename, $stream, $this->currentUser);
+    fclose($stream);
 
-    // This will take care of altering $file_uri if a file already exists.
-    file_unmanaged_prepare($temp_file_path, $file_uri);
-
-    // Lock based on the prepared file URI.
-    $lock_id = $this->generateLockIdFromFileUri($file_uri);
-
-    if (!$this->lock->acquire($lock_id)) {
-      throw new HttpException(503, sprintf('File "%s" is already locked for writing'), NULL, ['Retry-After' => 1]);
-    }
-
-    // Begin building file entity.
-    $file = File::create([]);
-    $file->setOwnerId($this->currentUser->id());
-    $file->setFilename($prepared_filename);
-    $file->setMimeType($this->mimeTypeGuesser->guess($prepared_filename));
-    $file->setFileUri($file_uri);
-    // Set the size. This is done in File::preSave() but we validate the file
-    // before it is saved.
-    $file->setSize(@filesize($temp_file_path));
-
-    // Validate the file entity against entity-level validation and field-level
-    // validators.
-    $this->validate($file, $validators);
-
-    // Move the file to the correct location after validation. Use
-    // FILE_EXISTS_ERROR as the file location has already been determined above
-    // in file_unmanaged_prepare().
-    if (!file_unmanaged_move($temp_file_path, $file_uri, FILE_EXISTS_ERROR)) {
-      throw new HttpException(500, 'Temporary file could not be moved to file location');
+    if ($file instanceof EntityConstraintViolationListInterface) {
+      $violations = $file;
+      $message = "Unprocessable Entity: file validation failed.\n";
+      $message .= implode("\n", array_map(function (ConstraintViolationInterface $violation) {
+        return PlainTextOutput::renderFromHtml($violation->getMessage());
+      }, (array) $violations->getIterator()));
+      throw new UnprocessableEntityHttpException($message);
     }
 
-    $file->save();
-
-    $this->lock->release($lock_id);
-
     // 201 Created responses return the newly created entity in the response
     // body. These responses are not cacheable, so we add no cacheability
     // metadata here.
     return new ModifiedResourceResponse($file, 201);
   }
 
-  /**
-   * Streams file upload data to temporary file and moves to file destination.
-   *
-   * @return string
-   *   The temp file path.
-   *
-   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
-   *   Thrown when input data cannot be read, the temporary file cannot be
-   *   opened, or the temporary file cannot be written.
-   */
-  protected function streamUploadData() {
-    // 'rb' is needed so reading works correctly on Windows environments too.
-    $file_data = fopen('php://input', 'rb');
-
-    $temp_file_path = $this->fileSystem->tempnam('temporary://', 'file');
-    $temp_file = fopen($temp_file_path, 'wb');
-
-    if ($temp_file) {
-      while (!feof($file_data)) {
-        $read = fread($file_data, static::BYTES_TO_READ);
-
-        if ($read === FALSE) {
-          // Close the file streams.
-          fclose($temp_file);
-          fclose($file_data);
-          $this->logger->error('Input data could not be read');
-          throw new HttpException(500, 'Input file data could not be read');
-        }
-
-        if (fwrite($temp_file, $read) === FALSE) {
-          // Close the file streams.
-          fclose($temp_file);
-          fclose($file_data);
-          $this->logger->error('Temporary file data for "%path" could not be written', ['%path' => $temp_file_path]);
-          throw new HttpException(500, 'Temporary file data could not be written');
-        }
-      }
-
-      // Close the temp file stream.
-      fclose($temp_file);
-    }
-    else {
-      // Close the file streams.
-      fclose($temp_file);
-      fclose($file_data);
-      $this->logger->error('Temporary file "%path" could not be opened for file upload', ['%path' => $temp_file_path]);
-      throw new HttpException(500, 'Temporary file could not be opened');
-    }
-
-    // Close the input stream.
-    fclose($file_data);
-
-    return $temp_file_path;
-  }
-
-  /**
-   * Validates and extracts the filename from the Content-Disposition header.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The request object.
-   *
-   * @return string
-   *   The filename extracted from the header.
-   *
-   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
-   *   Thrown when the 'Content-Disposition' request header is invalid.
-   */
-  protected function validateAndParseContentDispositionHeader(Request $request) {
-    // Firstly, check the header exists.
-    if (!$request->headers->has('content-disposition')) {
-      throw new BadRequestHttpException('"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided');
-    }
-
-    $content_disposition = $request->headers->get('content-disposition');
-
-    // Parse the header value. This regex does not allow an empty filename.
-    // i.e. 'filename=""'. This also matches on a word boundary so other keys
-    // like 'not_a_filename' don't work.
-    if (!preg_match(static::REQUEST_HEADER_FILENAME_REGEX, $content_disposition, $matches)) {
-      throw new BadRequestHttpException('No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided');
-    }
-
-    // Check for the "filename*" format. This is currently unsupported.
-    if (!empty($matches['star'])) {
-      throw new BadRequestHttpException('The extended "filename*" format is currently not supported in the "Content-Disposition" header');
-    }
-
-    // Don't validate the actual filename here, that will be done by the upload
-    // validators in validate().
-    // @see \Drupal\file\Plugin\rest\resource\FileUploadResource::validate()
-    $filename = $matches['filename'];
-
-    // Make sure only the filename component is returned. Path information is
-    // stripped as per https://tools.ietf.org/html/rfc6266#section-4.3.
-    return basename($filename);
-  }
-
   /**
    * Validates and loads a field definition instance.
    *
@@ -419,127 +296,6 @@ protected function validateAndLoadFieldDefinition($entity_type_id, $bundle, $fie
     return $field_definition;
   }
 
-  /**
-   * Validates the file.
-   *
-   * @param \Drupal\file\FileInterface $file
-   *   The file entity to validate.
-   * @param array $validators
-   *   An array of upload validators to pass to file_validate().
-   *
-   * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
-   *   Thrown when there are file validation errors.
-   */
-  protected function validate(FileInterface $file, array $validators) {
-    $this->resourceValidate($file);
-
-    // Validate the file based on the field definition configuration.
-    $errors = file_validate($file, $validators);
-
-    if (!empty($errors)) {
-      $message = "Unprocessable Entity: file validation failed.\n";
-      $message .= implode("\n", array_map(function ($error) {
-        return PlainTextOutput::renderFromHtml($error);
-      }, $errors));
-
-      throw new UnprocessableEntityHttpException($message);
-    }
-  }
-
-  /**
-   * Prepares the filename to strip out any malicious extensions.
-   *
-   * @param string $filename
-   *   The file name.
-   * @param array $validators
-   *   The array of upload validators.
-   *
-   * @return string
-   *   The prepared/munged filename.
-   */
-  protected function prepareFilename($filename, array &$validators) {
-    if (!empty($validators['file_validate_extensions'][0])) {
-      // If there is a file_validate_extensions validator and a list of
-      // valid extensions, munge the filename to protect against possible
-      // malicious extension hiding within an unknown file type. For example,
-      // "filename.html.foo".
-      $filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
-    }
-
-    // Rename potentially executable files, to help prevent exploits (i.e. will
-    // rename filename.php.foo and filename.php to filename.php.foo.txt and
-    // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
-    // evaluates to TRUE.
-    if (!$this->systemFileConfig->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename) && (substr($filename, -4) != '.txt')) {
-      // The destination filename will also later be used to create the URI.
-      $filename .= '.txt';
-
-      // The .txt extension may not be in the allowed list of extensions. We
-      // have to add it here or else the file upload will fail.
-      if (!empty($validators['file_validate_extensions'][0])) {
-        $validators['file_validate_extensions'][0] .= ' txt';
-      }
-    }
-
-    return $filename;
-  }
-
-  /**
-   * Determines the URI for a file field.
-   *
-   * @param array $settings
-   *   The array of field settings.
-   *
-   * @return string
-   *   An un-sanitized file directory URI with tokens replaced. The result of
-   *   the token replacement is then converted to plain text and returned.
-   */
-  protected function getUploadLocation(array $settings) {
-    $destination = trim($settings['file_directory'], '/');
-
-    // Replace tokens. As the tokens might contain HTML we convert it to plain
-    // text.
-    $destination = PlainTextOutput::renderFromHtml($this->token->replace($destination, []));
-    return $settings['uri_scheme'] . '://' . $destination;
-  }
-
-  /**
-   * Retrieves the upload validators for a field definition.
-   *
-   * This is copied from \Drupal\file\Plugin\Field\FieldType\FileItem as there
-   * is no entity instance available here that that a FileItem would exist for.
-   *
-   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
-   *   The field definition for which to get validators.
-   *
-   * @return array
-   *   An array suitable for passing to file_save_upload() or the file field
-   *   element's '#upload_validators' property.
-   */
-  protected function getUploadValidators(FieldDefinitionInterface $field_definition) {
-    $validators = [
-      // Add in our check of the file name length.
-      'file_validate_name_length' => [],
-    ];
-    $settings = $field_definition->getSettings();
-
-    // Cap the upload size according to the PHP limit.
-    $max_filesize = Bytes::toInt(file_upload_max_size());
-    if (!empty($settings['max_filesize'])) {
-      $max_filesize = min($max_filesize, Bytes::toInt($settings['max_filesize']));
-    }
-
-    // There is always a file size limit due to the PHP server limit.
-    $validators['file_validate_size'] = [$max_filesize];
-
-    // Add the extension check if necessary.
-    if (!empty($settings['file_extensions'])) {
-      $validators['file_validate_extensions'] = [$settings['file_extensions']];
-    }
-
-    return $validators;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -570,17 +326,4 @@ protected function getBaseRouteRequirements($method) {
     return $requirements;
   }
 
-  /**
-   * Generates a lock ID based on the file URI.
-   *
-   * @param $file_uri
-   *   The file URI.
-   *
-   * @return string
-   *   The generated lock ID.
-   */
-  protected static function generateLockIdFromFileUri($file_uri) {
-    return 'file:rest:' . Crypt::hashBase64($file_uri);
-  }
-
 }
diff --git a/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
index cb3ef3e27a..8dad10b2a0 100644
--- a/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
@@ -255,26 +255,26 @@ public function testPostFileUploadInvalidHeaders() {
 
     // An empty Content-Disposition header should return a 400.
     $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => FALSE]);
-    $this->assertResourceErrorResponse(400, '"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided', $response);
+    $this->assertResourceErrorResponse(400, '"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided.', $response);
 
     // An empty filename with a context in the Content-Disposition header should
     // return a 400.
     $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename=""']);
-    $this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided', $response);
+    $this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided.', $response);
 
     // An empty filename without a context in the Content-Disposition header
     // should return a 400.
     $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename=""']);
-    $this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided', $response);
+    $this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided.', $response);
 
     // An invalid key-value pair in the Content-Disposition header should return
     // a 400.
     $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'not_a_filename="example.txt"']);
-    $this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided', $response);
+    $this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided.', $response);
 
     // Using filename* extended format is not currently supported.
     $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename*="UTF-8 \' \' example.txt"']);
-    $this->assertResourceErrorResponse(400, 'The extended "filename*" format is currently not supported in the "Content-Disposition" header', $response);
+    $this->assertResourceErrorResponse(400, 'The extended "filename*" format is currently not supported in the "Content-Disposition" header.', $response);
   }
 
   /**
