diff --git a/core/includes/file.inc b/core/includes/file.inc
index e3ac8ea..47933db 100644
--- a/core/includes/file.inc
+++ b/core/includes/file.inc
@@ -7,9 +7,6 @@
 
 use Drupal\Core\StreamWrapper\LocalStream;
 use Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage;
-use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
-use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-use Symfony\Component\HttpFoundation\BinaryFileResponse;
 
 /**
  * Stream wrapper bit flags that are the basis for composite types.
@@ -1332,42 +1329,6 @@ function file_unmanaged_save_data($data, $destination = NULL, $replace = FILE_EX
 }
 
 /**
- * Page callback: Handles private file transfers.
- *
- * Call modules that implement hook_file_download() to find out if a file is
- * accessible and what headers it should be transferred with. If one or more
- * modules returned headers the download will start with the returned headers.
- * If a module returns -1 an AccessDeniedHttpException will be thrown.
- * If the file exists but no modules responded an AccessDeniedHttpException will
- * be thrown.If the file does not exist a NotFoundHttpException will be thrown.
- *
- * @see hook_file_download()
- * @see system_menu()
- */
-function file_download() {
-  // Merge remaining path arguments into relative file path.
-  $args = func_get_args();
-  $scheme = array_shift($args);
-  $target = implode('/', $args);
-  $uri = $scheme . '://' . $target;
-  if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
-    // Let other modules provide headers and controls access to the file.
-    $headers = module_invoke_all('file_download', $uri);
-    foreach ($headers as $result) {
-      if ($result == -1) {
-        throw new AccessDeniedHttpException();
-      }
-    }
-    if (count($headers)) {
-      return new BinaryFileResponse($uri, 200, $headers);
-    }
-    throw new AccessDeniedHttpException();
-  }
-  throw new NotFoundHttpException();
-}
-
-
-/**
  * Finds all files that match a given mask in a given directory.
  *
  * Directories and files beginning with a period are excluded; this
diff --git a/core/modules/system/lib/Drupal/system/FileController.php b/core/modules/system/lib/Drupal/system/FileController.php
new file mode 100644
index 0000000..67aeeff
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/FileController.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\FileController.
+ */
+
+namespace Drupal\system;
+
+use Drupal\Core\Extension\ModuleHandler;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
+
+/**
+ * System file controller.
+ */
+class FileController implements ControllerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('module_handler'));
+  }
+
+  /**
+   * @param \Drupal\Core\Extension\ModuleHandler $module_handler
+   *   The module handler.
+   */
+  public function __construct(ModuleHandler $module_handler) {
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * Handles private file transfers.
+   *
+   * Call modules that implement hook_file_download() to find out if a file is
+   * accessible and what headers it should be transferred with. If one or more
+   * modules returned headers the download will start with the returned headers.
+   * If a module returns -1 an AccessDeniedHttpException will be thrown. If the
+   * file exists but no modules responded an AccessDeniedHttpException will be
+   * thrown. If the file does not exist a NotFoundHttpException will be thrown.
+   *
+   * @see hook_file_download()
+   */
+  function fileDownload() {
+    // Merge remaining path arguments into relative file path.
+    $args = func_get_args();
+    $scheme = array_shift($args);
+    $target = implode('/', $args);
+    $uri = $scheme . '://' . $target;
+
+    if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
+      // Let other modules provide headers and controls access to the file.
+      $headers = $this->moduleHandler->invokeAll('file_download', $uri);
+
+      foreach ($headers as $result) {
+        if ($result == -1) {
+          throw new AccessDeniedHttpException();
+        }
+      }
+
+      if (count($headers)) {
+        return new BinaryFileResponse($uri, 200, $headers);
+      }
+
+      throw new AccessDeniedHttpException();
+    }
+
+    throw new NotFoundHttpException();
+  }
+
+}
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 4d2f61e..7e8bc78 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -617,13 +617,6 @@ function system_element_info() {
  * Implements hook_menu().
  */
 function system_menu() {
-  $items['system/files'] = array(
-    'title' => 'File download',
-    'page callback' => 'file_download',
-    'page arguments' => array('private'),
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
   $items['system/temporary'] = array(
     'title' => 'Temporary files',
     'page callback' => 'file_download',
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index b285844..b3f0c9d 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -73,3 +73,11 @@ system_site_maintenance_mode:
     _form: 'Drupal\system\Form\SiteMaintenanceModeForm'
   requirements:
     _permission: 'administer site configuration'
+
+system_files:
+  pattern: '/system/files'
+  defaults:
+    _controller: 'Drupal\system\FileController::fileDownload'
+    file_type: private
+  requirements:
+    _access: 'TRUE'
