diff --git a/core/lib/Drupal/Core/Entity/Exception/UnknownFieldException.php b/core/lib/Drupal/Core/Entity/Exception/UnknownFieldException.php
new file mode 100644
index 0000000000..90eeef71b0
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Exception/UnknownFieldException.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Drupal\Core\Entity\Exception;
+
+/**
+ * Exception thrown if a requested field does not exist.
+ */
+class UnknownFieldException extends \RuntimeException {}
diff --git a/core/modules/file/file.services.yml b/core/modules/file/file.services.yml
index 291edffb8f..c64a9363be 100644
--- a/core/modules/file/file.services.yml
+++ b/core/modules/file/file.services.yml
@@ -6,7 +6,22 @@ services:
       - { name: backend_overridable }
   file.upload_handler:
     class: Drupal\file\Upload\FileUploadHandler
-    arguments: [ '@file_system', '@entity_type.manager', '@stream_wrapper_manager', '@event_dispatcher', '@file.mime_type.guesser', '@current_user', '@request_stack' ]
+    arguments: [ '@file_system', '@file.repository', '@stream_wrapper_manager', '@event_dispatcher', '@file.mime_type.guesser', '@current_user', '@request_stack' ]
   file.repository:
     class: Drupal\file\FileRepository
     arguments: [ '@file_system', '@stream_wrapper_manager', '@entity_type.manager', '@module_handler', '@file.usage', '@current_user' ]
+  file.field_definition_resolver:
+    class: Drupal\file\FileFieldDefinitionResolver
+    arguments: [ '@entity_field.manager' ]
+  file.field_upload_handler:
+    class: Drupal\file\Upload\FileFieldUploadHandler
+    arguments: [ '@file_system', '@file.repository', '@stream_wrapper_manager', '@event_dispatcher', '@file.mime_type.guesser', '@current_user', '@request_stack', '@token' ]
+  file.raw_file_uploader:
+    class: Drupal\file\Upload\RawFileUploader
+    arguments: [ '@file_system' ]
+  file.upload_filename_extractor:
+    class: Drupal\file\Upload\FilenameExtractor
+    arguments: [ '@file_system' ]
+  file.upload_access_checker:
+    class: Drupal\file\Upload\FileFieldUploadAccessChecker
+    arguments: [ '@entity_type.manager', '@current_user' ]
diff --git a/core/modules/file/src/FileFieldDefinitionResolver.php b/core/modules/file/src/FileFieldDefinitionResolver.php
new file mode 100644
index 0000000000..1c2a34283d
--- /dev/null
+++ b/core/modules/file/src/FileFieldDefinitionResolver.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\file;
+
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\Exception\UnknownFieldException;
+use Drupal\Core\Field\FieldDefinitionInterface;
+
+/**
+ * Provides a File field resolver.
+ */
+class FileFieldDefinitionResolver {
+
+  /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * FileFieldResolver constructor.
+   *
+   *   The entity type manager.
+   *
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
+   *   The entity field manager.
+   */
+  public function __construct(EntityFieldManagerInterface $entityFieldManager) {
+    $this->entityFieldManager = $entityFieldManager;
+  }
+
+  /**
+   * Validates and loads a field definition instance.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID the field is attached to.
+   * @param string $bundle
+   *   The bundle the field is attached to.
+   * @param string $field_name
+   *   The field name.
+   *
+   * @return \Drupal\Core\Field\FieldDefinitionInterface
+   *   The field definition.
+   *
+   * @throws \Drupal\Core\Entity\Exception\UnknownFieldException
+   *   Thrown when the field does not exist.
+   * @throws \InvalidArgumentException
+   *   Thrown when the target type of the field is not a file.
+   */
+  public function resolveFieldDefinition(string $entity_type_id, string $bundle, string $field_name): FieldDefinitionInterface {
+    $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle);
+    if (!isset($field_definitions[$field_name])) {
+      throw new UnknownFieldException(sprintf('Field "%s" does not exist.', $field_name));
+    }
+
+    $field_definition = $field_definitions[$field_name];
+    if ($field_definition->getSetting('target_type') !== 'file') {
+      throw new \InvalidArgumentException(sprintf('"%s" is not a file field', $field_name));
+    }
+    return $field_definition;
+  }
+
+}
diff --git a/core/modules/file/src/Upload/BaseFileUploadHandler.php b/core/modules/file/src/Upload/BaseFileUploadHandler.php
new file mode 100644
index 0000000000..82d72e9b62
--- /dev/null
+++ b/core/modules/file/src/Upload/BaseFileUploadHandler.php
@@ -0,0 +1,276 @@
+<?php
+
+namespace Drupal\file\Upload;
+
+use Drupal\Core\File\Event\FileUploadSanitizeNameEvent;
+use Drupal\Core\File\Exception\FileExistsException;
+use Drupal\Core\File\Exception\FileWriteException;
+use Drupal\Core\File\Exception\InvalidStreamWrapperException;
+use Drupal\Core\File\FileSystemInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
+use Drupal\file\Entity\File;
+use Drupal\file\FileRepositoryInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException;
+use Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException;
+use Symfony\Component\HttpFoundation\File\Exception\FileException;
+use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException;
+use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException;
+use Symfony\Component\HttpFoundation\File\Exception\NoFileException;
+use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException;
+use Symfony\Component\HttpFoundation\File\Exception\PartialFileException;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\Mime\MimeTypeGuesserInterface;
+
+/**
+ * Provides a base class for file upload handlers.
+ */
+abstract class BaseFileUploadHandler {
+
+  /**
+   * The default extensions if none are provided.
+   */
+  const DEFAULT_EXTENSIONS = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
+
+  /**
+   * Constructs a FileUploadHandler object.
+   *
+   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
+   *   The file system service.
+   * @param \Drupal\file\FileRepositoryInterface $fileRepository
+   *   The file repository.
+   * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
+   *   The stream wrapper manager.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+   *   The event dispatcher.
+   * @param \Symfony\Component\Mime\MimeTypeGuesserInterface $mimeTypeGuesser
+   *   The MIME type guesser.
+   * @param \Drupal\Core\Session\AccountInterface $currentUser
+   *   The current user.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
+   *   The request stack.
+   */
+  public function __construct(
+    protected FileSystemInterface $fileSystem,
+    protected FileRepositoryInterface $fileRepository,
+    protected StreamWrapperManagerInterface $streamWrapperManager,
+    protected EventDispatcherInterface $eventDispatcher,
+    protected MimeTypeGuesserInterface $mimeTypeGuesser,
+    protected AccountInterface $currentUser,
+    protected RequestStack $requestStack) {
+  }
+
+  /**
+   * Creates a file from an upload.
+   *
+   * @param \Drupal\file\Upload\UploadedFileInterface $uploadedFile
+   *   The uploaded file object.
+   * @param array $validators
+   *   The validators to run against the uploaded file.
+   * @param string $destination
+   *   The destination directory.
+   * @param int $replace
+   *   Replace behavior when the destination file already exists:
+   *   - FileSystemInterface::EXISTS_REPLACE - Replace the existing file.
+   *   - FileSystemInterface::EXISTS_RENAME - Append _{incrementing number}
+   *     until the filename is unique.
+   *   - FileSystemInterface::EXISTS_ERROR - Throw an exception.
+   *
+   * @return \Drupal\file\Upload\FileUploadResult
+   *   The created file entity.
+   *
+   * @throws \Symfony\Component\HttpFoundation\File\Exception\FileException
+   *   Thrown when a file upload error occurred.
+   * @throws \Drupal\Core\File\Exception\FileWriteException
+   *   Thrown when there is an error moving the file.
+   * @throws \Drupal\Core\File\Exception\FileException
+   *   Thrown when a file system error occurs.
+   * @throws \Drupal\file\Upload\FileValidationException
+   *   Thrown when file validation fails.
+   */
+  public function handleFileUpload(UploadedFileInterface $uploadedFile, array $validators = [], string $destination = 'temporary://', int $replace = FileSystemInterface::EXISTS_REPLACE): FileUploadResult {
+    $originalName = $uploadedFile->getClientOriginalName();
+
+    if (!$uploadedFile->isValid()) {
+      switch ($uploadedFile->getError()) {
+        case \UPLOAD_ERR_INI_SIZE:
+          throw new IniSizeFileException($uploadedFile->getErrorMessage());
+
+        case \UPLOAD_ERR_FORM_SIZE:
+          throw new FormSizeFileException($uploadedFile->getErrorMessage());
+
+        case \UPLOAD_ERR_PARTIAL:
+          throw new PartialFileException($uploadedFile->getErrorMessage());
+
+        case \UPLOAD_ERR_NO_FILE:
+          throw new NoFileException($uploadedFile->getErrorMessage());
+
+        case \UPLOAD_ERR_CANT_WRITE:
+          throw new CannotWriteFileException($uploadedFile->getErrorMessage());
+
+        case \UPLOAD_ERR_NO_TMP_DIR:
+          throw new NoTmpDirFileException($uploadedFile->getErrorMessage());
+
+        case \UPLOAD_ERR_EXTENSION:
+          throw new ExtensionFileException($uploadedFile->getErrorMessage());
+
+      }
+
+      throw new FileException($uploadedFile->getErrorMessage());
+    }
+
+    $extensions = $this->handleExtensionValidation($validators);
+
+    // Assert that the destination contains a valid stream.
+    $destinationScheme = $this->streamWrapperManager::getScheme($destination);
+    if (!$this->streamWrapperManager->isValidScheme($destinationScheme)) {
+      throw new InvalidStreamWrapperException(sprintf('The file could not be uploaded because the destination "%s" is invalid.', $destination));
+    }
+
+    // A file URI may already have a trailing slash or look like "public://".
+    if (substr($destination, -1) != '/') {
+      $destination .= '/';
+    }
+
+    // Call an event to sanitize the filename and to attempt to address security
+    // issues caused by common server setups.
+    $event = new FileUploadSanitizeNameEvent($originalName, $extensions);
+    $this->eventDispatcher->dispatch($event);
+    $filename = $event->getFilename();
+
+    $mimeType = $this->mimeTypeGuesser->guessMimeType($filename);
+    $destinationFilename = $this->fileSystem->getDestinationFilename($destination . $filename, $replace);
+    if ($destinationFilename === FALSE) {
+      throw new FileExistsException(sprintf('Destination file "%s" exists', $destinationFilename));
+    }
+
+    $file = File::create([
+      'uid' => $this->currentUser->id(),
+      'status' => 0,
+      'uri' => $uploadedFile->getRealPath(),
+    ]);
+
+    // This will be replaced later with a filename based on the destination.
+    $file->setFilename($filename);
+    $file->setMimeType($mimeType);
+    $file->setSize($uploadedFile->getSize());
+
+    // Add in our check of the file name length.
+    $validators['file_validate_name_length'] = [];
+
+    // Call the validation functions specified by this function's caller.
+    $errors = file_validate($file, $validators);
+    if (!empty($errors)) {
+      throw new FileValidationException('File validation failed', $filename, $errors);
+    }
+
+    $file->setFileUri($destinationFilename);
+
+    if (!$this->moveUploadedFile($uploadedFile, $file->getFileUri())) {
+      throw new FileWriteException('File upload error. Could not move uploaded file.');
+    }
+
+    // Update the filename with any changes as a result of security or renaming
+    // due to an existing file.
+    $file->setFilename($this->fileSystem->basename($file->getFileUri()));
+
+    if ($replace === FileSystemInterface::EXISTS_REPLACE) {
+      $existingFile = $this->fileRepository->loadByUri($file->getFileUri());
+      if ($existingFile) {
+        $file->fid = $existingFile->id();
+        $file->setOriginalId($existingFile->id());
+      }
+    }
+
+    $result = (new FileUploadResult())
+      ->setOriginalFilename($originalName)
+      ->setSanitizedFilename($filename)
+      ->setFile($file);
+
+    // If the filename has been modified, let the user know.
+    if ($event->isSecurityRename()) {
+      $result->setSecurityRename();
+    }
+
+    // Set the permissions on the new file.
+    $this->fileSystem->chmod($file->getFileUri());
+
+    // We can now validate the file object itself before it's saved.
+    $violations = $file->validate();
+    foreach ($violations as $violation) {
+      $errors[] = $violation->getMessage();
+    }
+    if (!empty($errors)) {
+      throw new FileValidationException('File validation failed', $filename, $errors);
+    }
+
+    // If we made it this far it's safe to record this file in the database.
+    $file->save();
+
+    // Allow an anonymous user who creates a non-public file to see it. See
+    // \Drupal\file\FileAccessControlHandler::checkAccess().
+    if ($this->currentUser->isAnonymous() && $destinationScheme !== 'public') {
+      $session = $this->requestStack->getCurrentRequest()->getSession();
+      $allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
+      $allowed_temp_files[$file->id()] = $file->id();
+      $session->set('anonymous_allowed_file_ids', $allowed_temp_files);
+    }
+
+    return $result;
+  }
+
+  /**
+   * Move the uploaded file from the temporary path to the destination.
+   *
+   * @todo Allows a sub-class to override this method in order to handle
+   * raw file uploads in https://www.drupal.org/project/drupal/issues/2940383.
+   *
+   * @param \Drupal\file\Upload\UploadedFileInterface $uploadedFile
+   *   The uploaded file.
+   * @param string $uri
+   *   The destination URI.
+   *
+   * @return bool
+   *   Returns FALSE if moving failed.
+   *
+   * @see https://www.drupal.org/project/drupal/issues/2940383
+   */
+  abstract  protected function moveUploadedFile(UploadedFileInterface $uploadedFile, string $uri): bool;
+
+  /**
+   * Gets the list of allowed extensions and updates the validators.
+   *
+   * This will add an extension validator to the list of validators if one is
+   * not set.
+   *
+   * If the extension validator is set, but no extensions are specified, it
+   * means all extensions are allowed, so the validator is removed from the list
+   * of validators.
+   *
+   * @param array $validators
+   *   The file validators in use.
+   *
+   * @return string
+   *   The space delimited list of allowed file extensions.
+   */
+  protected function handleExtensionValidation(array &$validators): string {
+    // Build a list of allowed extensions.
+    if (isset($validators['file_validate_extensions'])) {
+      if (!isset($validators['file_validate_extensions'][0])) {
+        // If 'file_validate_extensions' is set and the list is empty then the
+        // caller wants to allow any extension. In this case we have to remove the
+        // validator or else it will reject all extensions.
+        unset($validators['file_validate_extensions']);
+      }
+    }
+    else {
+      // No validator was provided, so add one using the default list.
+      // Build a default non-munged safe list for
+      // \Drupal\system\EventSubscriber\SecurityFileUploadEventSubscriber::sanitizeName().
+      $validators['file_validate_extensions'] = [self::DEFAULT_EXTENSIONS];
+    }
+    return $validators['file_validate_extensions'][0] ?? '';
+  }
+
+}
diff --git a/core/modules/file/src/Upload/FileFieldUploadAccessChecker.php b/core/modules/file/src/Upload/FileFieldUploadAccessChecker.php
new file mode 100644
index 0000000000..bfd3393e55
--- /dev/null
+++ b/core/modules/file/src/Upload/FileFieldUploadAccessChecker.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\file\Upload;
+
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Provides an access checker for file field uploads.
+ */
+class FileFieldUploadAccessChecker {
+
+  /**
+   * Creates a new file field upload access checker.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager.
+   * @param \Drupal\Core\Session\AccountInterface $currentUser
+   *   The current user.
+   */
+  public function __construct(protected EntityTypeManagerInterface $entityTypeManager, protected AccountInterface $currentUser) {
+  }
+
+  /**
+   * Checks if the current user has access to upload the file.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $fieldDefinition
+   *   The field definition.
+   * @param \Drupal\Core\Entity\EntityInterface|null $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.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   */
+  public function checkFileUploadAccess(FieldDefinitionInterface $fieldDefinition, EntityInterface $entity = NULL): AccessResultInterface {
+    assert(is_null($entity) || $fieldDefinition->getTargetEntityTypeId() === $entity->getEntityTypeId() && $fieldDefinition->getTargetBundle() === $entity->bundle());
+    $entity_access_control_handler = $this->entityTypeManager->getAccessControlHandler($fieldDefinition->getTargetEntityTypeId());
+    $bundle = $this->entityTypeManager->getDefinition($fieldDefinition->getTargetEntityTypeId())
+      ->hasKey('bundle') ? $fieldDefinition->getTargetBundle() : NULL;
+    $entity_access_result = $entity
+      ? $entity_access_control_handler->access($entity, 'update', $this->currentUser, TRUE)
+      : $entity_access_control_handler->createAccess($bundle, $this->currentUser, [], TRUE);
+    $field_access_result = $entity_access_control_handler->fieldAccess('edit', $fieldDefinition, NULL, NULL, TRUE);
+    return $entity_access_result->andIf($field_access_result);
+  }
+
+}
diff --git a/core/modules/file/src/Upload/FileFieldUploadHandler.php b/core/modules/file/src/Upload/FileFieldUploadHandler.php
new file mode 100644
index 0000000000..e094125a7b
--- /dev/null
+++ b/core/modules/file/src/Upload/FileFieldUploadHandler.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace Drupal\file\Upload;
+
+use Drupal\Component\Render\PlainTextOutput;
+use Drupal\Component\Utility\Bytes;
+use Drupal\Component\Utility\Environment;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\File\Exception\DirectoryNotReadyException;
+use Drupal\Core\File\FileSystemInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
+use Drupal\Core\Utility\Token;
+use Drupal\file\FileRepositoryInterface;
+use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\Mime\MimeTypeGuesserInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * Provides an upload handler for file fields.
+ */
+class FileFieldUploadHandler extends BaseFileUploadHandler {
+
+  /**
+   * Constructs a FileUploadHandler object.
+   *
+   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
+   *   The file system service.
+   * @param \Drupal\file\FileRepositoryInterface $fileRepository
+   *   The file repository.
+   * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
+   *   The stream wrapper manager.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+   *   The event dispatcher.
+   * @param \Symfony\Component\Mime\MimeTypeGuesserInterface $mimeTypeGuesser
+   *   The MIME type guesser.
+   * @param \Drupal\Core\Session\AccountInterface $currentUser
+   *   The current user.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
+   *   The request stack.
+   * @param \Drupal\Core\Utility\Token $token
+   *   The token service.
+   */
+  public function __construct(FileSystemInterface $fileSystem, FileRepositoryInterface $fileRepository, StreamWrapperManagerInterface $streamWrapperManager, EventDispatcherInterface $eventDispatcher, MimeTypeGuesserInterface $mimeTypeGuesser, AccountInterface $currentUser, RequestStack $requestStack, protected Token $token) {
+    parent::__construct($fileSystem, $fileRepository, $streamWrapperManager, $eventDispatcher, $mimeTypeGuesser, $currentUser, $requestStack);
+  }
+
+  /**
+   * Handles file field uploads.
+   *
+   * @param \Drupal\file\Upload\UploadedFileInterface $uploadedFile
+   *   The uploaded file.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition.
+   *
+   * @return \Drupal\file\Upload\FileUploadResult
+   *   The created file entity.
+   *
+   * @throws \Symfony\Component\HttpFoundation\File\Exception\FileException
+   *   Thrown when a file upload error occurred.
+   * @throws \Drupal\Core\File\Exception\FileWriteException
+   *   Thrown when there is an error moving the file.
+   * @throws \Drupal\Core\File\Exception\FileException
+   *   Thrown when a file system error occurs.
+   * @throws \Drupal\file\Upload\FileValidationException
+   *   Thrown when file validation fails.
+   */
+  public function handleFileUploadForField(UploadedFileInterface $uploadedFile, FieldDefinitionInterface $field_definition): FileUploadResult {
+    assert(is_a($field_definition->getClass(), FileFieldItemList::class, TRUE));
+
+    $settings = $field_definition->getSettings();
+
+    $validators = $this->getUploadValidators($field_definition);
+    $destination = $this->getUploadLocation($settings);
+
+    // Check the destination file path is writable.
+    if (!$this->fileSystem->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY)) {
+      throw new DirectoryNotReadyException('Destination file path %s is not writable');
+    }
+
+    return $this->handleFileUpload($uploadedFile, $validators, $destination, FileSystemInterface::EXISTS_RENAME);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function moveUploadedFile($uploadedFile, $uri): bool {
+    return $this->fileSystem->move($uploadedFile->getPathname(), $uri);
+  }
+
+  /**
+   * 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): string {
+    $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 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): array {
+    $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::toNumber(Environment::getUploadMaxSize());
+    if (!empty($settings['max_filesize'])) {
+      $max_filesize = min($max_filesize, Bytes::toNumber($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 validator. The extension logic is handled later in
+    // FileUploadHandler::handleExtensionValidation.
+    $validators['file_validate_extensions'] = empty($settings['file_extensions']) ? [] : [$settings['file_extensions']];
+
+    return $validators;
+  }
+
+}
diff --git a/core/modules/file/src/Upload/FileUploadHandler.php b/core/modules/file/src/Upload/FileUploadHandler.php
index 28d883d444..c4cbc6d483 100644
--- a/core/modules/file/src/Upload/FileUploadHandler.php
+++ b/core/modules/file/src/Upload/FileUploadHandler.php
@@ -2,355 +2,16 @@
 
 namespace Drupal\file\Upload;
 
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\File\Event\FileUploadSanitizeNameEvent;
-use Drupal\Core\File\Exception\FileExistsException;
-use Drupal\Core\File\Exception\FileWriteException;
-use Drupal\Core\File\Exception\InvalidStreamWrapperException;
-use Drupal\Core\File\FileSystemInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
-use Drupal\file\Entity\File;
-use Drupal\file\FileInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException;
-use Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException;
-use Symfony\Component\HttpFoundation\File\Exception\FileException;
-use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException;
-use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException;
-use Symfony\Component\HttpFoundation\File\Exception\NoFileException;
-use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException;
-use Symfony\Component\HttpFoundation\File\Exception\PartialFileException;
-use Symfony\Component\HttpFoundation\RequestStack;
-use Symfony\Component\Mime\MimeTypeGuesserInterface;
-
 /**
  * Handles validating and creating file entities from file uploads.
  */
-class FileUploadHandler {
-
-  /**
-   * The default extensions if none are provided.
-   */
-  const DEFAULT_EXTENSIONS = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
-
-  /**
-   * The file system service.
-   *
-   * @var \Drupal\Core\File\FileSystemInterface
-   */
-  protected $fileSystem;
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * The stream wrapper manager.
-   *
-   * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
-   */
-  protected $streamWrapperManager;
-
-  /**
-   * The event dispatcher.
-   *
-   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
-   */
-  protected $eventDispatcher;
-
-  /**
-   * The current user.
-   *
-   * @var \Drupal\Core\Session\AccountInterface
-   */
-  protected $currentUser;
-
-  /**
-   * The MIME type guesser.
-   *
-   * @var \Symfony\Component\Mime\MimeTypeGuesserInterface
-   */
-  protected $mimeTypeGuesser;
-
-  /**
-   * The request stack.
-   *
-   * @var \Symfony\Component\HttpFoundation\RequestStack
-   */
-  protected $requestStack;
-
-  /**
-   * Constructs a FileUploadHandler object.
-   *
-   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
-   *   The file system service.
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
-   *   The entity type manager.
-   * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
-   *   The stream wrapper manager.
-   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
-   *   The event dispatcher.
-   * @param \Symfony\Component\Mime\MimeTypeGuesserInterface $mimeTypeGuesser
-   *   The MIME type guesser.
-   * @param \Drupal\Core\Session\AccountInterface $currentUser
-   *   The current user.
-   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
-   *   The request stack.
-   */
-  public function __construct(FileSystemInterface $fileSystem, EntityTypeManagerInterface $entityTypeManager, StreamWrapperManagerInterface $streamWrapperManager, EventDispatcherInterface $eventDispatcher, MimeTypeGuesserInterface $mimeTypeGuesser, AccountInterface $currentUser, RequestStack $requestStack) {
-    $this->fileSystem = $fileSystem;
-    $this->entityTypeManager = $entityTypeManager;
-    $this->streamWrapperManager = $streamWrapperManager;
-    $this->eventDispatcher = $eventDispatcher;
-    $this->mimeTypeGuesser = $mimeTypeGuesser;
-    $this->currentUser = $currentUser;
-    $this->requestStack = $requestStack;
-  }
-
-  /**
-   * Creates a file from an upload.
-   *
-   * @param \Drupal\file\Upload\UploadedFileInterface $uploadedFile
-   *   The uploaded file object.
-   * @param array $validators
-   *   The validators to run against the uploaded file.
-   * @param string $destination
-   *   The destination directory.
-   * @param int $replace
-   *   Replace behavior when the destination file already exists:
-   *   - FileSystemInterface::EXISTS_REPLACE - Replace the existing file.
-   *   - FileSystemInterface::EXISTS_RENAME - Append _{incrementing number}
-   *     until the filename is unique.
-   *   - FileSystemInterface::EXISTS_ERROR - Throw an exception.
-   *
-   * @return \Drupal\file\Upload\FileUploadResult
-   *   The created file entity.
-   *
-   * @throws \Symfony\Component\HttpFoundation\File\Exception\FileException
-   *   Thrown when a file upload error occurred.
-   * @throws \Drupal\Core\File\Exception\FileWriteException
-   *   Thrown when there is an error moving the file.
-   * @throws \Drupal\Core\File\Exception\FileException
-   *   Thrown when a file system error occurs.
-   * @throws \Drupal\file\Upload\FileValidationException
-   *   Thrown when file validation fails.
-   */
-  public function handleFileUpload(UploadedFileInterface $uploadedFile, array $validators = [], string $destination = 'temporary://', int $replace = FileSystemInterface::EXISTS_REPLACE): FileUploadResult {
-    $originalName = $uploadedFile->getClientOriginalName();
-
-    if (!$uploadedFile->isValid()) {
-      switch ($uploadedFile->getError()) {
-        case \UPLOAD_ERR_INI_SIZE:
-          throw new IniSizeFileException($uploadedFile->getErrorMessage());
-
-        case \UPLOAD_ERR_FORM_SIZE:
-          throw new FormSizeFileException($uploadedFile->getErrorMessage());
-
-        case \UPLOAD_ERR_PARTIAL:
-          throw new PartialFileException($uploadedFile->getErrorMessage());
-
-        case \UPLOAD_ERR_NO_FILE:
-          throw new NoFileException($uploadedFile->getErrorMessage());
-
-        case \UPLOAD_ERR_CANT_WRITE:
-          throw new CannotWriteFileException($uploadedFile->getErrorMessage());
-
-        case \UPLOAD_ERR_NO_TMP_DIR:
-          throw new NoTmpDirFileException($uploadedFile->getErrorMessage());
-
-        case \UPLOAD_ERR_EXTENSION:
-          throw new ExtensionFileException($uploadedFile->getErrorMessage());
-
-      }
-
-      throw new FileException($uploadedFile->getErrorMessage());
-    }
-
-    $extensions = $this->handleExtensionValidation($validators);
-
-    // Assert that the destination contains a valid stream.
-    $destinationScheme = $this->streamWrapperManager::getScheme($destination);
-    if (!$this->streamWrapperManager->isValidScheme($destinationScheme)) {
-      throw new InvalidStreamWrapperException(sprintf('The file could not be uploaded because the destination "%s" is invalid.', $destination));
-    }
-
-    // A file URI may already have a trailing slash or look like "public://".
-    if (substr($destination, -1) != '/') {
-      $destination .= '/';
-    }
-
-    // Call an event to sanitize the filename and to attempt to address security
-    // issues caused by common server setups.
-    $event = new FileUploadSanitizeNameEvent($originalName, $extensions);
-    $this->eventDispatcher->dispatch($event);
-    $filename = $event->getFilename();
-
-    $mimeType = $this->mimeTypeGuesser->guessMimeType($filename);
-    $destinationFilename = $this->fileSystem->getDestinationFilename($destination . $filename, $replace);
-    if ($destinationFilename === FALSE) {
-      throw new FileExistsException(sprintf('Destination file "%s" exists', $destinationFilename));
-    }
-
-    $file = File::create([
-      'uid' => $this->currentUser->id(),
-      'status' => 0,
-      'uri' => $uploadedFile->getRealPath(),
-    ]);
-
-    // This will be replaced later with a filename based on the destination.
-    $file->setFilename($filename);
-    $file->setMimeType($mimeType);
-    $file->setSize($uploadedFile->getSize());
-
-    // Add in our check of the file name length.
-    $validators['file_validate_name_length'] = [];
-
-    // Call the validation functions specified by this function's caller.
-    $errors = file_validate($file, $validators);
-    if (!empty($errors)) {
-      throw new FileValidationException('File validation failed', $filename, $errors);
-    }
-
-    $file->setFileUri($destinationFilename);
-
-    if (!$this->moveUploadedFile($uploadedFile, $file->getFileUri())) {
-      throw new FileWriteException('File upload error. Could not move uploaded file.');
-    }
-
-    // Update the filename with any changes as a result of security or renaming
-    // due to an existing file.
-    $file->setFilename($this->fileSystem->basename($file->getFileUri()));
-
-    if ($replace === FileSystemInterface::EXISTS_REPLACE) {
-      $existingFile = $this->loadByUri($file->getFileUri());
-      if ($existingFile) {
-        $file->fid = $existingFile->id();
-        $file->setOriginalId($existingFile->id());
-      }
-    }
-
-    $result = (new FileUploadResult())
-      ->setOriginalFilename($originalName)
-      ->setSanitizedFilename($filename)
-      ->setFile($file);
-
-    // If the filename has been modified, let the user know.
-    if ($event->isSecurityRename()) {
-      $result->setSecurityRename();
-    }
-
-    // Set the permissions on the new file.
-    $this->fileSystem->chmod($file->getFileUri());
-
-    // We can now validate the file object itself before it's saved.
-    $violations = $file->validate();
-    foreach ($violations as $violation) {
-      $errors[] = $violation->getMessage();
-    }
-    if (!empty($errors)) {
-      throw new FileValidationException('File validation failed', $filename, $errors);
-    }
-
-    // If we made it this far it's safe to record this file in the database.
-    $file->save();
-
-    // Allow an anonymous user who creates a non-public file to see it. See
-    // \Drupal\file\FileAccessControlHandler::checkAccess().
-    if ($this->currentUser->isAnonymous() && $destinationScheme !== 'public') {
-      $session = $this->requestStack->getCurrentRequest()->getSession();
-      $allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
-      $allowed_temp_files[$file->id()] = $file->id();
-      $session->set('anonymous_allowed_file_ids', $allowed_temp_files);
-    }
-
-    return $result;
-  }
+class FileUploadHandler extends BaseFileUploadHandler {
 
   /**
-   * Move the uploaded file from the temporary path to the destination.
-   *
-   * @todo Allows a sub-class to override this method in order to handle
-   * raw file uploads in https://www.drupal.org/project/drupal/issues/2940383.
-   *
-   * @param \Drupal\file\Upload\UploadedFileInterface $uploadedFile
-   *   The uploaded file.
-   * @param string $uri
-   *   The destination URI.
-   *
-   * @return bool
-   *   Returns FALSE if moving failed.
-   *
-   * @see https://www.drupal.org/project/drupal/issues/2940383
+   * {@inheritdoc}
    */
-  protected function moveUploadedFile(UploadedFileInterface $uploadedFile, string $uri) {
+  protected function moveUploadedFile(UploadedFileInterface $uploadedFile, string $uri): bool {
     return $this->fileSystem->moveUploadedFile($uploadedFile->getRealPath(), $uri);
   }
 
-  /**
-   * Gets the list of allowed extensions and updates the validators.
-   *
-   * This will add an extension validator to the list of validators if one is
-   * not set.
-   *
-   * If the extension validator is set, but no extensions are specified, it
-   * means all extensions are allowed, so the validator is removed from the list
-   * of validators.
-   *
-   * @param array $validators
-   *   The file validators in use.
-   *
-   * @return string
-   *   The space delimited list of allowed file extensions.
-   */
-  protected function handleExtensionValidation(array &$validators): string {
-    // Build a list of allowed extensions.
-    if (isset($validators['file_validate_extensions'])) {
-      if (!isset($validators['file_validate_extensions'][0])) {
-        // If 'file_validate_extensions' is set and the list is empty then the
-        // caller wants to allow any extension. In this case we have to remove the
-        // validator or else it will reject all extensions.
-        unset($validators['file_validate_extensions']);
-      }
-    }
-    else {
-      // No validator was provided, so add one using the default list.
-      // Build a default non-munged safe list for
-      // \Drupal\system\EventSubscriber\SecurityFileUploadEventSubscriber::sanitizeName().
-      $validators['file_validate_extensions'] = [self::DEFAULT_EXTENSIONS];
-    }
-    return $validators['file_validate_extensions'][0] ?? '';
-  }
-
-  /**
-   * Loads the first File entity found with the specified URI.
-   *
-   * @param string $uri
-   *   The file URI.
-   *
-   * @return \Drupal\file\FileInterface|null
-   *   The first file with the matched URI if found, NULL otherwise.
-   *
-   * @todo replace with https://www.drupal.org/project/drupal/issues/3223209
-   */
-  protected function loadByUri(string $uri): ?FileInterface {
-    $fileStorage = $this->entityTypeManager->getStorage('file');
-    /** @var \Drupal\file\FileInterface[] $files */
-    $files = $fileStorage->loadByProperties(['uri' => $uri]);
-    if (count($files)) {
-      foreach ($files as $item) {
-        // Since some database servers sometimes use a case-insensitive
-        // comparison by default, double check that the filename is an exact
-        // match.
-        if ($item->getFileUri() === $uri) {
-          return $item;
-        }
-      }
-    }
-    return NULL;
-  }
-
 }
diff --git a/core/modules/file/src/Upload/FilenameExtractor.php b/core/modules/file/src/Upload/FilenameExtractor.php
new file mode 100644
index 0000000000..63420a5be4
--- /dev/null
+++ b/core/modules/file/src/Upload/FilenameExtractor.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Drupal\file\Upload;
+
+use Drupal\Core\File\FileSystemInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * Extracts the filename from the request headers.
+ */
+class FilenameExtractor {
+
+  /**
+   * The regex used to extract the filename from the content disposition header.
+   *
+   * @var string
+   */
+  const REQUEST_HEADER_FILENAME_REGEX = '@\bfilename(?<star>\*?)=\"(?<filename>.+)\"@';
+
+  /**
+   * Creates a new file name extractor.
+   *
+   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
+   *   The file system service.
+   */
+  public function __construct(protected FileSystemInterface $fileSystem) {
+  }
+
+  /**
+   * Validate and extract the filename from the request headers.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   *
+   * @return string
+   *   The filename.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   *   Thrown when there is an error extracting the filename.
+   */
+  public function extractFilename(Request $request): string {
+    // 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 $this->fileSystem->basename($filename);
+  }
+
+}
diff --git a/core/modules/file/src/Upload/FormUploadedFile.php b/core/modules/file/src/Upload/FormUploadedFile.php
index fb777b158d..90345054df 100644
--- a/core/modules/file/src/Upload/FormUploadedFile.php
+++ b/core/modules/file/src/Upload/FormUploadedFile.php
@@ -9,21 +9,13 @@
  */
 class FormUploadedFile implements UploadedFileInterface {
 
-  /**
-   * The wrapped uploaded file.
-   *
-   * @var \Symfony\Component\HttpFoundation\File\UploadedFile
-   */
-  protected $uploadedFile;
-
   /**
    * Creates a new FormUploadedFile.
    *
    * @param \Symfony\Component\HttpFoundation\File\UploadedFile $uploadedFile
    *   The wrapped Symfony uploaded file.
    */
-  public function __construct(UploadedFile $uploadedFile) {
-    $this->uploadedFile = $uploadedFile;
+  public function __construct(protected UploadedFile $uploadedFile) {
   }
 
   /**
@@ -57,14 +49,14 @@ public function getError(): int {
   /**
    * {@inheritdoc}
    */
-  public function getSize(): int {
+  public function getSize(): false|int {
     return $this->uploadedFile->getSize();
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getRealPath() {
+  public function getRealPath(): false|string {
     return $this->uploadedFile->getRealPath();
   }
 
diff --git a/core/modules/file/src/Upload/RawFileUploader.php b/core/modules/file/src/Upload/RawFileUploader.php
new file mode 100644
index 0000000000..eb13e29dca
--- /dev/null
+++ b/core/modules/file/src/Upload/RawFileUploader.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\file\Upload;
+
+use Drupal\Core\File\FileSystemInterface;
+
+/**
+ * Uploads raw file data.
+ */
+class RawFileUploader {
+
+  /**
+   * The amount of bytes to read in each iteration when streaming file data.
+   *
+   * @var int
+   */
+  const BYTES_TO_READ = 8192;
+
+  /**
+   * Creates a new raw file uploader.
+   *
+   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
+   *   The file system service.
+   */
+  public function __construct(protected FileSystemInterface $fileSystem) {
+  }
+
+  /**
+   * Upload file data from the input stream.
+   *
+   * @param string $filename
+   *   The original file name of the uploaded file.
+   * @param string $source
+   *   (Optional) The source of the upload. Defaults to 'php://input'.
+   * @param string $mimeType
+   *   (Optional) The type of the file as provided by PHP; null defaults to
+   *   application/octet-stream.
+   *
+   * @return \Drupal\file\Upload\RawUploadedFile
+   *   The raw uploaded file.
+   */
+  public function uploadRawFile(string $filename, string $source = 'php://input', string $mimeType = 'application/octet-stream'): RawUploadedFile {
+    // 'rb' is needed so reading works correctly on Windows environments too.
+    $file_data = fopen($source, 'rb');
+
+    $temp_file_path = $this->fileSystem->tempnam('temporary://', 'file');
+    if ($temp_file_path === FALSE) {
+      return new RawUploadedFile('temporary://', $filename, $mimeType, \UPLOAD_ERR_NO_TMP_DIR);
+    }
+
+    $temp_file = fopen($temp_file_path, 'wb');
+
+    if ($temp_file === FALSE) {
+      // Close the input file stream since we can't proceed with the upload.
+      // Don't try to close $temp_file since it's FALSE at this point.
+      fclose($file_data);
+      return new RawUploadedFile($temp_file_path, $filename, $mimeType, \UPLOAD_ERR_NO_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);
+        return new RawUploadedFile($temp_file, $filename, $mimeType, \UPLOAD_ERR_NO_FILE);
+      }
+
+      if (fwrite($temp_file, $read) === FALSE) {
+        // Close the file streams.
+        fclose($temp_file);
+        fclose($file_data);
+        return new RawUploadedFile($temp_file_path, $filename, $mimeType, \UPLOAD_ERR_CANT_WRITE);
+      }
+    }
+
+    // Close the temp file stream.
+    fclose($temp_file);
+
+    // Close the input stream.
+    fclose($file_data);
+
+    return new RawUploadedFile($temp_file_path, $filename);
+  }
+
+}
diff --git a/core/modules/file/src/Upload/RawUploadedFile.php b/core/modules/file/src/Upload/RawUploadedFile.php
new file mode 100644
index 0000000000..f68744c02e
--- /dev/null
+++ b/core/modules/file/src/Upload/RawUploadedFile.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\file\Upload;
+
+use Symfony\Component\HttpFoundation\File\File;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+/**
+ * Implements a raw uploaded file that was not uploaded via a form.
+ *
+ * @see https://www.php.net/manual/en/wrappers.php.php#wrappers.php.input
+ */
+class RawUploadedFile extends File implements UploadedFileInterface {
+
+  /**
+   * Constructs a new raw uploaded file.
+   *
+   * @param string $path
+   *   The path to the file.
+   * @param string $originalName
+   *   The original name provided by the client.
+   * @param string $mimeType
+   *   The untrusted mimetype provided by the client.
+   * @param int $error
+   *   The error code.
+   */
+  public function __construct(string $path, protected string $originalName, protected string $mimeType = 'application/octet-stream', protected int $error = \UPLOAD_ERR_OK) {
+    parent::__construct($path, \UPLOAD_ERR_OK === $this->error);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getClientOriginalName(): string {
+    return $this->originalName;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isValid(): bool {
+    return \UPLOAD_ERR_OK === $this->error;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @see \Symfony\Component\HttpFoundation\File\UploadedFile::getErrorMessage()
+   */
+  public function getErrorMessage(): string {
+    static $errors = [
+      \UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).',
+      \UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.',
+      \UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.',
+      \UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
+      \UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.',
+      \UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.',
+      \UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.',
+    ];
+
+    $errorCode = $this->error;
+    $maxFilesize = \UPLOAD_ERR_INI_SIZE === $errorCode ? UploadedFile::getMaxFilesize() / 1024 : 0;
+    $message = $errors[$errorCode] ?? 'The file "%s" was not uploaded due to an unknown error.';
+
+    return sprintf($message, $this->getClientOriginalName(), $maxFilesize);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getError(): int {
+    return $this->error;
+  }
+
+}
diff --git a/core/modules/file/src/Upload/UploadedFileInterface.php b/core/modules/file/src/Upload/UploadedFileInterface.php
index b20b8490ad..e85e4f47f4 100644
--- a/core/modules/file/src/Upload/UploadedFileInterface.php
+++ b/core/modules/file/src/Upload/UploadedFileInterface.php
@@ -48,12 +48,12 @@ public function getError(): int;
   /**
    * Gets file size.
    *
-   * @return int
+   * @return int|false
    *   The filesize in bytes.
    *
    * @see https://www.php.net/manual/en/splfileinfo.getsize.php
    */
-  public function getSize(): int;
+  public function getSize(): false|int;
 
   /**
    * Gets the absolute path to the file.
@@ -63,7 +63,7 @@ public function getSize(): int;
    *
    * @see https://php.net/manual/en/splfileinfo.getrealpath.php
    */
-  public function getRealPath();
+  public function getRealPath(): bool|string;
 
   /**
    * Gets the path to the file.
diff --git a/core/modules/file/tests/src/Kernel/FileFieldUploadHandlerTest.php b/core/modules/file/tests/src/Kernel/FileFieldUploadHandlerTest.php
new file mode 100644
index 0000000000..16aaa055cf
--- /dev/null
+++ b/core/modules/file/tests/src/Kernel/FileFieldUploadHandlerTest.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\Tests\file\Kernel;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
+use Drupal\file\Upload\RawUploadedFile;
+use org\bovigo\vfs\vfsStream;
+
+/**
+ * Tests the file field upload handler.
+ *
+ * @coversDefaultClass \Drupal\file\Upload\FileFieldUploadHandler
+ * @group file
+ */
+class FileFieldUploadHandlerTest extends FileManagedUnitTestBase {
+
+  /**
+   * The file field upload handler under test.
+   *
+   * @var \Drupal\file\Upload\FileFieldUploadHandler
+   */
+  protected $fileFieldUploadHandler;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->fileFieldUploadHandler = $this->container->get('file.field_upload_handler');
+  }
+
+  /**
+   * @covers ::handleFileUploadForField
+   */
+  public function testHandleFileUploadForField() {
+
+    $vfsRoot = vfsStream::setup();
+    $tempFile = vfsStream::newFile("temp1234")->at($vfsRoot);
+
+    $filename = $this->randomMachineName() . ".txt";
+    $uploadedFile = new RawUploadedFile($tempFile->url(), $filename);
+
+    $fileDir = $this->randomMachineName();
+
+    $fieldDefinition = $this->prophesize(FieldDefinitionInterface::class);
+    $fieldDefinition->getClass()
+      ->willReturn(FileFieldItemList::class);
+    $fieldDefinition->getSettings()
+      ->willReturn([
+        'file_directory' => $fileDir,
+        'uri_scheme' => "public",
+        'max_filesize' => "1024",
+      ]);
+
+    $result = $this->fileFieldUploadHandler->handleFileUploadForField($uploadedFile, $fieldDefinition->reveal());
+
+    $this->assertNotNull($result);
+
+    $file = $result->getFile();
+    $this->assertNotNull($file);
+
+    $this->assertEquals($filename, $file->getFilename());
+    $this->assertEquals("public://$fileDir/$filename", $file->getFileUri());
+  }
+
+}
diff --git a/core/modules/file/tests/src/Kernel/Upload/FileFieldUploadAccessCheckerTest.php b/core/modules/file/tests/src/Kernel/Upload/FileFieldUploadAccessCheckerTest.php
new file mode 100644
index 0000000000..6910dd905c
--- /dev/null
+++ b/core/modules/file/tests/src/Kernel/Upload/FileFieldUploadAccessCheckerTest.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Drupal\Tests\file\Kernel\Upload;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\file\Upload\FileFieldUploadAccessChecker;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+use Drupal\Tests\user\Traits\UserCreationTrait;
+
+/**
+ * Tests for file upload access control.
+ *
+ * @group file
+ */
+class FileFieldUploadAccessCheckerTest extends KernelTestBase {
+
+  use UserCreationTrait;
+  use NodeCreationTrait;
+  use EntityReferenceTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'node',
+    'file',
+    'field',
+    'serialization',
+    'system',
+    'user',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    // Add the entity schemas.
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+    // Add the additional table schemas.
+    $this->installSchema('system', ['sequences']);
+    $this->installSchema('node', ['node_access']);
+    $this->installSchema('user', ['users_data']);
+
+    $type = NodeType::create([
+      'type' => 'article',
+    ]);
+    $type->save();
+    $type = NodeType::create([
+      'type' => 'page',
+    ]);
+    $type->save();
+
+    $this->createEntityReferenceField('node', 'article', 'field_relationships', 'Relationship', 'node', 'default', ['target_bundles' => ['article']], FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+  }
+
+  /**
+   * @covers ::checkFileUploadAccess
+   */
+  public function testCheckFileUploadAccessWithBaseField() {
+    $accessChecker = $this->container->get('file.upload_access_checker');
+    assert($accessChecker instanceof FileFieldUploadAccessChecker);
+
+    // Create admin user to so next user isn't user 1.
+    $this->createUser([], NULL, TRUE);
+
+    $article_editor = $this->createUser([
+      'access content',
+      'create article content',
+      'edit any article content',
+    ]);
+
+    $page_editor = $this->createUser([
+      'access content',
+      'create page content',
+      'edit any page content',
+    ]);
+
+    $editor = $this->createUser([
+      'bypass node access',
+    ]);
+
+    $no_access_user = $this->createUser();
+
+    $node = $this->createNode([
+      'type' => 'article',
+      'uid' => 1,
+    ]);
+
+    // While the method is only used to check file fields it should work without
+    // error for any field whether it is a base field or a bundle field.
+    $base_field_definition = $this->container->get('entity_field.manager')
+      ->getBaseFieldDefinitions('node')['title'];
+    $bundle_field_definition = $this->container->get('entity_field.manager')
+      ->getFieldDefinitions('node', 'article')['field_relationships'];
+
+    // Tests the expected access result for each user.
+    // The $article_editor account can edit any article.
+    $this->setCurrentUser($article_editor);
+
+    $result = $accessChecker->checkFileUploadAccess($base_field_definition, $node);
+    $this->assertTrue($result->isAllowed());
+    // The article editor cannot create a node of undetermined type.
+    $result = $accessChecker->checkFileUploadAccess($base_field_definition);
+    $this->assertFalse($result->isAllowed());
+    // The article editor can edit any article.
+    $result = $accessChecker->checkFileUploadAccess($bundle_field_definition, $node);
+    $this->assertTrue($result->isAllowed());
+    // The article editor can create an article. The type can be determined
+    // because the field is a bundle field.
+    $result = $accessChecker->checkFileUploadAccess($bundle_field_definition);
+    $this->assertTrue($result->isAllowed());
+
+    // The $editor account has the bypass node access permissions and can edit
+    // and create all node types.
+    $this->setCurrentUser($editor);
+
+    $result = $accessChecker->checkFileUploadAccess($base_field_definition, $node);
+    $this->assertTrue($result->isAllowed());
+    $result = $accessChecker->checkFileUploadAccess($base_field_definition);
+    $this->assertTrue($result->isAllowed());
+    $result = $accessChecker->checkFileUploadAccess($bundle_field_definition, $node);
+    $this->assertTrue($result->isAllowed());
+    $result = $accessChecker->checkFileUploadAccess($bundle_field_definition);
+    $this->assertTrue($result->isAllowed());
+
+    // The $page_editor account can only edit and create pages therefore has no
+    // access.
+    $this->setCurrentUser($page_editor);
+
+    $result = $accessChecker->checkFileUploadAccess($base_field_definition, $node);
+    $this->assertFalse($result->isAllowed());
+    $result = $accessChecker->checkFileUploadAccess($base_field_definition);
+    $this->assertFalse($result->isAllowed());
+    $result = $accessChecker->checkFileUploadAccess($bundle_field_definition, $node);
+    $this->assertFalse($result->isAllowed());
+    $result = $accessChecker->checkFileUploadAccess($bundle_field_definition);
+    $this->assertFalse($result->isAllowed());
+
+    // The $no_access_user account has no access at all.
+    $this->setCurrentUser($no_access_user);
+
+    $result = $accessChecker->checkFileUploadAccess($base_field_definition, $node);
+    $this->assertFalse($result->isAllowed());
+    $result = $accessChecker->checkFileUploadAccess($base_field_definition);
+    $this->assertFalse($result->isAllowed());
+    $result = $accessChecker->checkFileUploadAccess($bundle_field_definition, $node);
+    $this->assertFalse($result->isAllowed());
+    $result = $accessChecker->checkFileUploadAccess($bundle_field_definition);
+    $this->assertFalse($result->isAllowed());
+  }
+
+}
diff --git a/core/modules/file/tests/src/Unit/FileFieldDefinitionResolverTest.php b/core/modules/file/tests/src/Unit/FileFieldDefinitionResolverTest.php
new file mode 100644
index 0000000000..884dbd81a0
--- /dev/null
+++ b/core/modules/file/tests/src/Unit/FileFieldDefinitionResolverTest.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\Tests\file\Unit;
+
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\Exception\UnknownFieldException;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\file\FileFieldDefinitionResolver;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests for file field definition resolver.
+ *
+ * @coversDefaultClass \Drupal\file\FileFieldDefinitionResolver
+ * @group file
+ */
+class FileFieldDefinitionResolverTest extends UnitTestCase {
+
+  /**
+   * @covers ::resolveFieldDefinition
+   */
+  public function testResolveFieldDefinitionSuccess() {
+
+    $field_definition = $this->prophesize(FieldDefinitionInterface::class);
+    $field_definition->getSetting('target_type')
+      ->willReturn("file");
+
+    $definitions = [
+      "baz" => $field_definition->reveal(),
+    ];
+
+    $entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class);
+    $entityFieldManager->getFieldDefinitions("foo", "bar")
+      ->willReturn($definitions);
+    $resolver = new FileFieldDefinitionResolver($entityFieldManager->reveal());
+
+    $definition = $resolver->resolveFieldDefinition("foo", "bar", "baz");
+
+    $this->assertNotNull($definition);
+  }
+
+  /**
+   * @covers ::resolveFieldDefinition
+   */
+  public function testResolveFieldDefinitionUnknownField() {
+
+    $entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class);
+    $entityFieldManager->getFieldDefinitions("foo", "bar")
+      ->willReturn([]);
+
+    $resolver = new FileFieldDefinitionResolver($entityFieldManager->reveal());
+
+    $field_name = "baz";
+
+    $this->expectException(UnknownFieldException::class);
+    $this->expectExceptionMessage(sprintf('Field "%s" does not exist.', $field_name));
+
+    $resolver->resolveFieldDefinition("foo", "bar", $field_name);
+  }
+
+  /**
+   * @covers ::resolveFieldDefinition
+   */
+  public function testResolveFieldDefinitionInvalidTargetType() {
+
+    $field_definition = $this->prophesize(FieldDefinitionInterface::class);
+    $field_definition->getSetting('target_type')
+      ->willReturn("whiz");
+
+    $field_name = "baz";
+
+    $definitions = [
+      $field_name => $field_definition->reveal(),
+    ];
+
+    $entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class);
+    $entityFieldManager->getFieldDefinitions("foo", "bar")
+      ->willReturn($definitions);
+    $resolver = new FileFieldDefinitionResolver($entityFieldManager->reveal());
+
+    $this->expectException(\InvalidArgumentException::class);
+    $this->expectExceptionMessage(sprintf('"%s" is not a file field', $field_name));
+    $resolver->resolveFieldDefinition("foo", "bar", $field_name);
+
+  }
+
+}
diff --git a/core/modules/file/tests/src/Unit/Upload/FileFieldUploadAccessCheckerTest.php b/core/modules/file/tests/src/Unit/Upload/FileFieldUploadAccessCheckerTest.php
new file mode 100644
index 0000000000..4579796fb4
--- /dev/null
+++ b/core/modules/file/tests/src/Unit/Upload/FileFieldUploadAccessCheckerTest.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Drupal\Tests\file\Unit\Upload;
+
+use Drupal\Core\Access\AccessResultAllowed;
+use Drupal\Core\Entity\EntityAccessControlHandlerInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\file\Upload\FileFieldUploadAccessChecker;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * Tests the file field upload access checker.
+ *
+ * @coversDefaultClass \Drupal\file\Upload\FileFieldUploadAccessChecker
+ * @group file
+ */
+class FileFieldUploadAccessCheckerTest extends UnitTestCase {
+
+  /**
+   * @covers ::checkFileUploadAccess
+   */
+  public function testCheckAccessSuccess() {
+
+    $entityAccess = new AccessResultAllowed();
+    $fieldAccess = new AccessResultAllowed();
+
+    $entityTypeId = "foo";
+    $bundle = "bar";
+    $currentUser = $this->prophesize(AccountInterface::class);
+
+    $fieldDefinition = $this->prophesize(FieldDefinitionInterface::class);
+    $fieldDefinition->getTargetEntityTypeId()
+      ->willReturn($entityTypeId);
+    $fieldDefinition->getTargetBundle()
+      ->willReturn($bundle);
+
+    $entityAccessControlHandler = $this->prophesize(EntityAccessControlHandlerInterface::class);
+    $entityAccessControlHandler->createAccess($bundle, $currentUser->reveal(), [], TRUE)
+      ->willReturn($entityAccess);
+    $entityAccessControlHandler->fieldAccess("edit", $fieldDefinition->reveal(), NULL, NULL, TRUE)
+      ->willReturn($fieldAccess);
+
+    $entityType = $this->prophesize(EntityTypeInterface::class);
+    $entityType->hasKey(Argument::any())
+      ->willReturn(TRUE);
+
+    $entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
+    $entityTypeManager->getAccessControlHandler($entityTypeId)
+      ->willReturn($entityAccessControlHandler->reveal());
+    $entityTypeManager->getDefinition($entityTypeId)
+      ->willReturn($entityType->reveal());
+
+    $checker = new FileFieldUploadAccessChecker($entityTypeManager->reveal(), $currentUser->reveal());
+
+    $accessResult = $checker->checkFileUploadAccess($fieldDefinition->reveal());
+
+    $this->assertTrue($accessResult->isAllowed());
+  }
+
+  /**
+   * @covers ::checkFileUploadAccess
+   */
+  public function testCheckAccessWithEntitySuccess() {
+
+    $entityAccess = new AccessResultAllowed();
+    $fieldAccess = new AccessResultAllowed();
+
+    $bundle = "bar";
+    $entityTypeId = "foo";
+    $entity = $this->prophesize(EntityInterface::class);
+    $entity->bundle()
+      ->willReturn($bundle);
+    $entity->getEntityTypeId()
+      ->willReturn($entityTypeId);
+
+    $currentUser = $this->prophesize(AccountInterface::class);
+
+    $fieldDefinition = $this->prophesize(FieldDefinitionInterface::class);
+    $fieldDefinition->getTargetEntityTypeId()
+      ->willReturn($entityTypeId);
+    $fieldDefinition->getTargetBundle()
+      ->willReturn($bundle);
+
+    $entityAccessControlHandler = $this->prophesize(EntityAccessControlHandlerInterface::class);
+    $entityAccessControlHandler->access($entity->reveal(), "update", $currentUser->reveal(), TRUE)
+      ->willReturn($entityAccess);
+    $entityAccessControlHandler->fieldAccess("edit", $fieldDefinition->reveal(), NULL, NULL, TRUE)
+      ->willReturn($fieldAccess);
+
+    $entityType = $this->prophesize(EntityTypeInterface::class);
+    $entityType->hasKey(Argument::any())
+      ->willReturn(TRUE);
+
+    $entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
+    $entityTypeManager->getAccessControlHandler($entityTypeId)
+      ->willReturn($entityAccessControlHandler->reveal());
+    $entityTypeManager->getDefinition($entityTypeId)
+      ->willReturn($entityType->reveal());
+
+    $checker = new FileFieldUploadAccessChecker($entityTypeManager->reveal(), $currentUser->reveal());
+
+    $accessResult = $checker->checkFileUploadAccess($fieldDefinition->reveal(), $entity->reveal());
+
+    $this->assertTrue($accessResult->isAllowed());
+  }
+
+}
diff --git a/core/modules/file/tests/src/Unit/Upload/FilenameExtractorTest.php b/core/modules/file/tests/src/Unit/Upload/FilenameExtractorTest.php
new file mode 100644
index 0000000000..924b36b8b4
--- /dev/null
+++ b/core/modules/file/tests/src/Unit/Upload/FilenameExtractorTest.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\Tests\file\Unit\Upload;
+
+use Drupal\Core\File\FileSystemInterface;
+use Drupal\file\Upload\FilenameExtractor;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * Tests the filename extractor.
+ *
+ * @coversDefaultClass \Drupal\file\Upload\FilenameExtractor
+ * @group file
+ */
+class FilenameExtractorTest extends UnitTestCase {
+
+  /**
+   * @covers ::extractFilename
+   */
+  public function testExtractFilenameSuccess() {
+
+    $fileSystem = $this->prophesize(FileSystemInterface::class);
+    $fileSystem->basename(Argument::any())
+      ->willReturn("foo.txt");
+
+    $extractor = new FilenameExtractor($fileSystem->reveal());
+
+    $request = new Request();
+    $request->headers->add(['content-disposition' => 'filename="foo.txt"']);
+
+    $filename = $extractor->extractFilename($request);
+
+    $this->assertEquals("foo.txt", $filename);
+  }
+
+  /**
+   * @covers ::extractFilename
+   */
+  public function testExtractFilenameMissingHeader() {
+
+    $fileSystem = $this->prophesize(FileSystemInterface::class);
+
+    $extractor = new FilenameExtractor($fileSystem->reveal());
+
+    $request = new Request();
+
+    $this->expectException(BadRequestHttpException::class);
+    $this->expectExceptionMessage('"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided.');
+
+    $extractor->extractFilename($request);
+  }
+
+  /**
+   * @covers ::extractFilename
+   */
+  public function testExtractFilenameBadHeader() {
+
+    $fileSystem = $this->prophesize(FileSystemInterface::class);
+
+    $extractor = new FilenameExtractor($fileSystem->reveal());
+
+    $request = new Request();
+    $request->headers->add(['content-disposition' => 'foo']);
+
+    $this->expectException(BadRequestHttpException::class);
+    $this->expectExceptionMessage('No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided.');
+
+    $extractor->extractFilename($request);
+  }
+
+  /**
+   * @covers ::extractFilename
+   */
+  public function testExtractFilenameBadHeaderWithStar() {
+
+    $fileSystem = $this->prophesize(FileSystemInterface::class);
+
+    $extractor = new FilenameExtractor($fileSystem->reveal());
+
+    $request = new Request();
+    $request->headers->add(['content-disposition' => 'filename*="foo.txt"']);
+
+    $this->expectException(BadRequestHttpException::class);
+    $this->expectExceptionMessage('The extended "filename*" format is currently not supported in the "Content-Disposition" header.');
+
+    $extractor->extractFilename($request);
+  }
+
+}
diff --git a/core/modules/file/tests/src/Unit/Upload/RawFileUploaderTest.php b/core/modules/file/tests/src/Unit/Upload/RawFileUploaderTest.php
new file mode 100644
index 0000000000..ae45236431
--- /dev/null
+++ b/core/modules/file/tests/src/Unit/Upload/RawFileUploaderTest.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\Tests\file\Unit\Upload;
+
+use Drupal\Core\File\FileSystemInterface;
+use Drupal\file\Upload\RawFileUploader;
+use Drupal\Tests\UnitTestCase;
+use org\bovigo\vfs\vfsStream;
+use Prophecy\Argument;
+
+/**
+ * Tests the file stream uploader.
+ *
+ * @coversDefaultClass \Drupal\file\Upload\RawFileUploader
+ * @group file
+ */
+class RawFileUploaderTest extends UnitTestCase {
+
+  /**
+   * @covers ::uploadRawFile
+   */
+  public function testUpload() {
+
+    $vfsRoot = vfsStream::setup();
+    $tempFile = vfsStream::newFile("temp1234")->at($vfsRoot);
+
+    $fileSystem = $this->prophesize(FileSystemInterface::class);
+    $fileSystem->tempnam(Argument::any(), Argument::any())
+      ->willReturn($tempFile->url());
+
+    $uploader = new RawFileUploader($fileSystem->reveal());
+
+    $filename = "foo.txt";
+    $input = vfsStream::newFile($filename)->at($vfsRoot)->setContent("foo");
+
+    $uploadedFile = $uploader->uploadRawFile($filename, $input->url());
+
+    $this->assertNotNull($uploadedFile);
+    $this->assertEquals($filename, $uploadedFile->getClientOriginalName());
+    $this->assertEquals(\UPLOAD_ERR_OK, $uploadedFile->getError());
+  }
+
+}
diff --git a/core/modules/jsonapi/jsonapi.services.yml b/core/modules/jsonapi/jsonapi.services.yml
index e54be7f253..7df1f3e5e4 100644
--- a/core/modules/jsonapi/jsonapi.services.yml
+++ b/core/modules/jsonapi/jsonapi.services.yml
@@ -186,13 +186,6 @@ services:
       - '@jsonapi.serializer'
       - '@datetime.time'
       - '@current_user'
-  jsonapi.file_upload:
-    class: Drupal\jsonapi\Controller\FileUpload
-    arguments:
-      - '@current_user'
-      - '@entity_field.manager'
-      - '@jsonapi.file.uploader.field'
-      - '@http_kernel'
 
   # Event subscribers.
   jsonapi.custom_query_parameter_names_validator.subscriber:
diff --git a/core/modules/jsonapi/src/Controller/FileUpload.php b/core/modules/jsonapi/src/Controller/FileUpload.php
index c11244a799..67d72f1838 100644
--- a/core/modules/jsonapi/src/Controller/FileUpload.php
+++ b/core/modules/jsonapi/src/Controller/FileUpload.php
@@ -5,12 +5,20 @@
 use Drupal\Component\Render\PlainTextOutput;
 use Drupal\Core\Access\AccessResultReasonInterface;
 use Drupal\Core\Cache\CacheableMetadata;
-use Drupal\Core\Entity\EntityConstraintViolationListInterface;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\Exception\UnknownFieldException;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\File\Exception\FileException;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
+use Drupal\file\FileFieldDefinitionResolver;
+use Drupal\file\FileInterface;
+use Drupal\file\Upload\FileFieldUploadAccessChecker;
+use Drupal\file\Upload\FileFieldUploadHandler;
+use Drupal\file\Upload\FilenameExtractor;
+use Drupal\file\Upload\FileValidationException;
+use Drupal\file\Upload\RawFileUploader;
 use Drupal\jsonapi\Entity\EntityValidationTrait;
 use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
 use Drupal\jsonapi\JsonApiResource\Link;
@@ -20,22 +28,23 @@
 use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
 use Drupal\jsonapi\ResourceResponse;
 use Drupal\jsonapi\ResourceType\ResourceType;
-use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpFoundation\File\Exception\FileException as SymfonyFileException;
 use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\HttpKernel\Exception\HttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
-use Symfony\Component\Validator\ConstraintViolationInterface;
 
 /**
  * Handles file upload requests.
  *
- * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
- *   may change at any time and could break any dependencies on it.
- *
  * @see https://www.drupal.org/project/drupal/issues/3032787
  * @see jsonapi.api.php
+ * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
+ *   may change at any time and could break any dependencies on it.
  */
 class FileUpload {
 
@@ -69,6 +78,46 @@ class FileUpload {
    */
   protected $httpKernel;
 
+  /**
+   * The file field upload handler.
+   *
+   * @var \Drupal\file\Upload\FileFieldUploadHandler
+   */
+  protected $fileFieldUploadHandler;
+
+  /**
+   * @var \Drupal\file\FileFieldDefinitionResolver
+   */
+  protected $fieldResolver;
+
+  /**
+   * The file field upload access checker.
+   *
+   * @var \Drupal\file\Upload\FileFieldUploadAccessChecker
+   */
+  protected $uploadAccessChecker;
+
+  /**
+   * The filename extractor.
+   *
+   * @var \Drupal\file\Upload\FilenameExtractor
+   */
+  protected $filenameExtractor;
+
+  /**
+   * The file stream uploader.
+   *
+   * @var \Drupal\file\Upload\RawFileUploader
+   */
+  protected $fileStreamUploader;
+
+  /**
+   * The logger.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
   /**
    * Creates a new FileUpload instance.
    *
@@ -80,12 +129,30 @@ class FileUpload {
    *   The file uploader.
    * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
    *   An HTTP kernel for making subrequests.
+   * @param \Drupal\file\Upload\FileFieldUploadHandler $upload_handler
+   *   The file field upload handler.
+   * @param \Drupal\file\FileFieldDefinitionResolver $field_resolver
+   *   The file field definition resolver.
+   * @param \Drupal\file\Upload\FileFieldUploadAccessChecker $access_checker
+   *   The file upload access checker.
+   * @param \Drupal\file\Upload\FilenameExtractor $filename_extractor
+   *   The filename extractor.
+   * @param \Drupal\file\Upload\RawFileUploader $file_stream_uploader
+   *   The file stream uploader.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger.
    */
-  public function __construct(AccountInterface $current_user, EntityFieldManagerInterface $field_manager, TemporaryJsonapiFileFieldUploader $file_uploader, HttpKernelInterface $http_kernel) {
+  public function __construct(AccountInterface $current_user, EntityFieldManagerInterface $field_manager, TemporaryJsonapiFileFieldUploader $file_uploader, HttpKernelInterface $http_kernel, FileFieldUploadHandler $upload_handler, FileFieldDefinitionResolver $field_resolver, FileFieldUploadAccessChecker $access_checker, FilenameExtractor $filename_extractor, RawFileUploader $file_stream_uploader, LoggerInterface $logger) {
     $this->currentUser = $current_user;
     $this->fieldManager = $field_manager;
     $this->fileUploader = $file_uploader;
     $this->httpKernel = $http_kernel;
+    $this->fileFieldUploadHandler = $upload_handler;
+    $this->fieldResolver = $field_resolver;
+    $this->uploadAccessChecker = $access_checker;
+    $this->filenameExtractor = $filename_extractor;
+    $this->fileStreamUploader = $file_stream_uploader;
+    $this->logger = $logger;
   }
 
   /**
@@ -111,23 +178,8 @@ public function __construct(AccountInterface $current_user, EntityFieldManagerIn
    *   Thrown if an exception occurs during a subrequest to fetch the newly
    *   created file entity.
    */
-  public function handleFileUploadForExistingResource(Request $request, ResourceType $resource_type, $file_field_name, FieldableEntityInterface $entity) {
-    $file_field_name = $resource_type->getInternalName($file_field_name);
-    $field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name);
-
-    static::ensureFileUploadAccess($this->currentUser, $field_definition, $entity);
-
-    $filename = $this->fileUploader->validateAndParseContentDispositionHeader($request);
-    $file = $this->fileUploader->handleFileUploadForField($field_definition, $filename, $this->currentUser);
-
-    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);
-    }
+  public function handleFileUploadForExistingResource(Request $request, ResourceType $resource_type, string $file_field_name, FieldableEntityInterface $entity): Response {
+    $file = $this->doHandleFileUpload($request, $resource_type, $file_field_name, $entity);
 
     if ($resource_type->getFieldByInternalName($file_field_name)->hasOne()) {
       $entity->{$file_field_name} = $file;
@@ -140,7 +192,8 @@ public function handleFileUploadForExistingResource(Request $request, ResourceTy
 
     $route_parameters = ['entity' => $entity->uuid()];
     $route_name = sprintf('jsonapi.%s.%s.related', $resource_type->getTypeName(), $resource_type->getPublicName($file_field_name));
-    $related_url = Url::fromRoute($route_name, $route_parameters)->toString(TRUE);
+    $related_url = Url::fromRoute($route_name, $route_parameters)
+      ->toString(TRUE);
     $request = Request::create($related_url->getGeneratedUrl(), 'GET', [], $request->cookies->all(), [], $request->server->all());
     return $this->httpKernel->handle($request, HttpKernelInterface::SUB_REQUEST);
   }
@@ -161,23 +214,8 @@ public function handleFileUploadForExistingResource(Request $request, ResourceTy
    * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
    *   Thrown when there are validation errors.
    */
-  public function handleFileUploadForNewResource(Request $request, ResourceType $resource_type, $file_field_name) {
-    $file_field_name = $resource_type->getInternalName($file_field_name);
-    $field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name);
-
-    static::ensureFileUploadAccess($this->currentUser, $field_definition);
-
-    $filename = $this->fileUploader->validateAndParseContentDispositionHeader($request);
-    $file = $this->fileUploader->handleFileUploadForField($field_definition, $filename, $this->currentUser);
-
-    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());
-      }, iterator_to_array($violations)));
-      throw new UnprocessableEntityHttpException($message);
-    }
+  public function handleFileUploadForNewResource(Request $request, ResourceType $resource_type, string $file_field_name): Response {
+    $file = $this->doHandleFileUpload($request, $resource_type, $file_field_name);
 
     // @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463.
     $self_link = new Link(new CacheableMetadata(), Url::fromRoute('jsonapi.file--file.individual', ['entity' => $file->uuid()]), 'self');
@@ -190,20 +228,67 @@ public function handleFileUploadForNewResource(Request $request, ResourceType $r
     return new ResourceResponse(new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), $links), 201, []);
   }
 
+  /**
+   * Handles common logic of file uploads.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The HTTP request object.
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The JSON:API resource type for the current request.
+   * @param string $file_field_name
+   *   The file field for which the file is to be uploaded.
+   * @param \Drupal\Core\Entity\FieldableEntityInterface|null $entity
+   *   The entity.
+   *
+   * @return \Drupal\file\FileInterface
+   */
+  protected function doHandleFileUpload(Request $request, ResourceType $resource_type, string $file_field_name, FieldableEntityInterface $entity = NULL): FileInterface {
+    $file_field_name = $resource_type->getInternalName($file_field_name);
+
+    try {
+      $field_definition = $this->fieldResolver->resolveFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name);
+    }
+    catch (UnknownFieldException $e) {
+      throw new NotFoundHttpException($e->getMessage(), $e);
+    }
+
+    $this->ensureFileUploadAccess($field_definition, $entity);
+
+    $filename = $this->filenameExtractor->extractFilename($request);
+    $uploadedFile = $this->fileStreamUploader->uploadRawFile($filename);
+
+    try {
+      $result = $this->fileFieldUploadHandler->handleFileUploadForField($uploadedFile, $field_definition);
+    }
+    catch (SymfonyFileException | FileException $e) {
+      $this->logger->error('Temporary file could not be created for file upload.');
+      throw new HttpException(500, $e->getMessage(), $e);
+    }
+    catch (FileValidationException $e) {
+      $message = "Unprocessable Entity: file validation failed.\n";
+      $message .= implode("\n", array_map(function (string $error) {
+        return PlainTextOutput::renderFromHtml($error);
+      }, $e->getErrors()));
+
+      throw new UnprocessableEntityHttpException($message);
+    }
+
+    return $result->getFile();
+  }
+
   /**
    * Ensures that the given account is allowed to upload a file.
    *
-   * @param \Drupal\Core\Session\AccountInterface $account
-   *   The account for which access should be checked.
    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
    *   The field for which the file is to be uploaded.
    * @param \Drupal\Core\Entity\FieldableEntityInterface|null $entity
    *   The entity, if one exists, for which the file is to be uploaded.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
+   *   Thrown if access is denied.
    */
-  protected static function ensureFileUploadAccess(AccountInterface $account, FieldDefinitionInterface $field_definition, FieldableEntityInterface $entity = NULL) {
-    $access_result = $entity
-      ? TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($account, $field_definition, $entity)
-      : TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($account, $field_definition);
+  protected function ensureFileUploadAccess(FieldDefinitionInterface $field_definition, FieldableEntityInterface $entity = NULL): void {
+    $access_result = $this->uploadAccessChecker->checkFileUploadAccess($field_definition, $entity);
     if (!$access_result->isAllowed()) {
       $reason = 'The current user is not permitted to upload a file for this field.';
       if ($access_result instanceof AccessResultReasonInterface) {
@@ -213,38 +298,4 @@ protected static function ensureFileUploadAccess(AccountInterface $account, Fiel
     }
   }
 
-  /**
-   * Validates and loads a field definition instance.
-   *
-   * @param string $entity_type_id
-   *   The entity type ID the field is attached to.
-   * @param string $bundle
-   *   The bundle the field is attached to.
-   * @param string $field_name
-   *   The field name.
-   *
-   * @return \Drupal\Core\Field\FieldDefinitionInterface
-   *   The field definition.
-   *
-   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
-   *   Thrown when the field does not exist.
-   * @throws \Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException
-   *   Thrown when the target type of the field is not a file, or the current
-   *   user does not have 'edit' access for the field.
-   */
-  protected function validateAndLoadFieldDefinition($entity_type_id, $bundle, $field_name) {
-    $field_definitions = $this->fieldManager->getFieldDefinitions($entity_type_id, $bundle);
-    if (!isset($field_definitions[$field_name])) {
-      throw new NotFoundHttpException(sprintf('Field "%s" does not exist.', $field_name));
-    }
-
-    /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
-    $field_definition = $field_definitions[$field_name];
-    if ($field_definition->getSetting('target_type') !== 'file') {
-      throw new AccessDeniedException(sprintf('"%s" is not a file field', $field_name));
-    }
-
-    return $field_definition;
-  }
-
 }
diff --git a/core/modules/jsonapi/src/JsonapiServiceProvider.php b/core/modules/jsonapi/src/JsonapiServiceProvider.php
index e3b0f6d0cc..8cced88db2 100644
--- a/core/modules/jsonapi/src/JsonapiServiceProvider.php
+++ b/core/modules/jsonapi/src/JsonapiServiceProvider.php
@@ -6,16 +6,17 @@
 use Drupal\Core\DependencyInjection\ServiceModifierInterface;
 use Drupal\Core\DependencyInjection\ServiceProviderInterface;
 use Drupal\Core\StackMiddleware\NegotiationMiddleware;
+use Drupal\jsonapi\Controller\FileUpload;
 use Drupal\jsonapi\DependencyInjection\Compiler\RegisterSerializationClassesCompilerPass;
+use Symfony\Component\DependencyInjection\Reference;
 
 /**
  * Adds 'api_json' as known format and prevents its use in the REST module.
  *
- * @internal JSON:API maintains no PHP API since its API is the HTTP API. This
- *   class may change at any time and this will break any dependencies on it.
- *
  * @see https://www.drupal.org/project/drupal/issues/3032787
  * @see jsonapi.api.php
+ * @internal JSON:API maintains no PHP API since its API is the HTTP API. This
+ *   class may change at any time and this will break any dependencies on it.
  */
 class JsonapiServiceProvider implements ServiceModifierInterface, ServiceProviderInterface {
 
@@ -23,7 +24,8 @@ class JsonapiServiceProvider implements ServiceModifierInterface, ServiceProvide
    * {@inheritdoc}
    */
   public function alter(ContainerBuilder $container) {
-    if ($container->has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')->getClass(), NegotiationMiddleware::class, TRUE)) {
+    if ($container->has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')
+      ->getClass(), NegotiationMiddleware::class, TRUE)) {
       // @see http://www.iana.org/assignments/media-types/application/vnd.api+json
       $container->getDefinition('http_middleware.negotiation')
         ->addMethodCall('registerFormat', [
@@ -38,6 +40,21 @@ public function alter(ContainerBuilder $container) {
    */
   public function register(ContainerBuilder $container) {
     $container->addCompilerPass(new RegisterSerializationClassesCompilerPass());
+    // Only register file_upload service if the file module is enabled.
+    $modules = $container->getParameter('container.modules');
+    if (isset($modules['file'])) {
+      $container->register('jsonapi.file_upload', FileUpload::class)
+        ->addArgument(new Reference('current_user'))
+        ->addArgument(new Reference('entity_field.manager'))
+        ->addArgument(new Reference('jsonapi.file.uploader.field'))
+        ->addArgument(new Reference('http_kernel'))
+        ->addArgument(new Reference('file.field_upload_handler'))
+        ->addArgument(new Reference('file.field_definition_resolver'))
+        ->addArgument(new Reference('file.upload_access_checker'))
+        ->addArgument(new Reference('file.upload_filename_extractor'))
+        ->addArgument(new Reference('file.raw_file_uploader'))
+        ->addArgument(new Reference('logger.channel.file'));
+    }
   }
 
 }
