diff --git a/core/includes/file.inc b/core/includes/file.inc index f141cc81d714afd354f17f076ffaed2c91981d84..a26f21f1ee7fc8d3b98780da91d62c99b567b265 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -109,8 +109,10 @@ * - filesize: The size of the file in bytes. * - status: A bitmapped field indicating the status of the file. The first 8 * bits are reserved for Drupal core. The least significant bit indicates - * temporary (0) or permanent (1). Temporary files older than - * DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during cron runs. + * temporary (0) or permanent (1). Temporary files will be removed during + * cron runs if they are older than the configuration value + * "system.file.temporary_maximum_age", and if clean-up is enabled. Permanent + * files will not be removed. * - timestamp: UNIX timestamp for the date the file was added to the database. */ @@ -142,9 +144,10 @@ /** * Indicates that the file is permanent and should not be deleted. * - * Temporary files older than DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed - * during cron runs, but permanent files will not be removed during the file - * garbage collection process. + * Temporary files older than the system.file.temporary_maximum_age + * configuration value will be, if clean-up not disabled, removed during cron + * runs, but permanent files will not be removed during the file garbage + * collection process. */ const FILE_STATUS_PERMANENT = 1; diff --git a/core/modules/file/file.install b/core/modules/file/file.install index fb5a4eb8d61fedbeaf3da37ea89871daf1c94451..6bbed7915888fa58bd1e091a46981f236eb9ad82 100644 --- a/core/modules/file/file.install +++ b/core/modules/file/file.install @@ -69,7 +69,7 @@ function file_schema() { 'default' => 0, ), 'status' => array( - 'description' => 'A field indicating the status of the file. Two status are defined in core: temporary (0) and permanent (1). Temporary files older than DRUPAL_MAXIMUM_TEMP_FILE_AGE will be removed during a cron run.', + 'description' => 'A field indicating the status of the file. Two status are defined in core: temporary (0) and permanent (1). Temporary files older than system.file.temporary_maximum_age will be removed during a cron run.', 'type' => 'int', 'not null' => TRUE, 'default' => 0, diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 389086287eddf56c6e97250f3bf8fe5410e51c9a..2543616acd28a86c1594099d984c68b8ed16f65c 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -694,9 +694,18 @@ function file_file_download($uri, $field_type = 'file') { * Implements file_cron() */ function file_cron() { - $result = \Drupal::entityManager()->getStorage('file')->retrieveTemporaryFiles(); - foreach ($result as $row) { - if ($file = file_load($row->fid)) { + $age = \Drupal::config('system.file')->get('temporary_maximum_age'); + + // Only delete temporary files if older than $age. Note that automatic cleanup + // is disabled if $age set to 0. + if ($age) { + $fids = Drupal::entityQuery('file') + ->condition('status', FILE_STATUS_PERMANENT, '<>') + ->condition('changed', REQUEST_TIME - $age, '<') + ->range(0, 100) + ->execute(); + $files = file_load_multiple($fids); + foreach ($files as $file) { $references = \Drupal::service('file.usage')->listUsage($file); if (empty($references)) { if (file_exists($file->getFileUri())) { diff --git a/core/modules/file/lib/Drupal/file/Tests/DeleteTest.php b/core/modules/file/lib/Drupal/file/Tests/DeleteTest.php index f345811e47325256f6dc3bb5f2bc650eebc11459..f7889b52ac8af99b9ab07f64c7fc1b71395b18c8 100644 --- a/core/modules/file/lib/Drupal/file/Tests/DeleteTest.php +++ b/core/modules/file/lib/Drupal/file/Tests/DeleteTest.php @@ -61,17 +61,18 @@ function testInUse() { $this->assertTrue($file->isTemporary(), 'File is temporary.'); file_test_reset(); - // Call system_cron() to clean up the file. Make sure the timestamp - // of the file is older than DRUPAL_MAXIMUM_TEMP_FILE_AGE. + // Call file_cron() to clean up the file. Make sure the changed timestamp + // of the file is older than the system.file.temporary_maximum_age + // configuration value. db_update('file_managed') ->fields(array( - 'changed' => REQUEST_TIME - (DRUPAL_MAXIMUM_TEMP_FILE_AGE + 1), + 'changed' => REQUEST_TIME - ($this->container->get('config.factory')->get('system.file')->get('temporary_maximum_age') + 1), )) ->condition('fid', $file->id()) ->execute(); \Drupal::service('cron')->run(); - // system_cron() loads + // file_cron() loads $this->assertFileHooksCalled(array('delete')); $this->assertFalse(file_exists($file->getFileUri()), 'File has been deleted after its last usage was removed.'); $this->assertFalse(file_load($file->id()), 'File was removed from the database.'); diff --git a/core/modules/file/lib/Drupal/file/Tests/FileFieldRevisionTest.php b/core/modules/file/lib/Drupal/file/Tests/FileFieldRevisionTest.php index f5864466465562c8aa209c8ced1013bf8ee75135..070217b8bc90ae78bd6b929dd612a93406787327 100644 --- a/core/modules/file/lib/Drupal/file/Tests/FileFieldRevisionTest.php +++ b/core/modules/file/lib/Drupal/file/Tests/FileFieldRevisionTest.php @@ -115,11 +115,12 @@ function testRevisions() { clearstatcache($node_file_r3->getFileUri()); clearstatcache($node_file_r4->getFileUri()); - // Call system_cron() to clean up the file. Make sure the timestamp - // of the file is older than DRUPAL_MAXIMUM_TEMP_FILE_AGE. + // Call file_cron() to clean up the file. Make sure the changed timestamp + // of the file is older than the system.file.temporary_maximum_age + // configuration value. db_update('file_managed') ->fields(array( - 'changed' => REQUEST_TIME - (DRUPAL_MAXIMUM_TEMP_FILE_AGE + 1), + 'changed' => REQUEST_TIME - ($this->container->get('config.factory')->get('system.file')->get('temporary_maximum_age') + 1), )) ->condition('fid', $node_file_r3->id()) ->execute(); @@ -130,11 +131,12 @@ function testRevisions() { // Delete the entire node and check that the original file is deleted. $this->drupalPostForm('node/' . $nid . '/delete', array(), t('Delete')); - // Call system_cron() to clean up the file. Make sure the timestamp - // of the file is older than DRUPAL_MAXIMUM_TEMP_FILE_AGE. + // Call file_cron() to clean up the file. Make sure the changed timestamp + // of the file is older than the system.file.temporary_maximum_age + // configuration value. db_update('file_managed') ->fields(array( - 'changed' => REQUEST_TIME - (DRUPAL_MAXIMUM_TEMP_FILE_AGE + 1), + 'changed' => REQUEST_TIME - ($this->container->get('config.factory')->get('system.file')->get('temporary_maximum_age') + 1), )) ->condition('fid', $node_file_r1->id()) ->execute(); diff --git a/core/modules/file/lib/Drupal/file/Tests/UsageTest.php b/core/modules/file/lib/Drupal/file/Tests/UsageTest.php index 3cff7da4108503882da3294b362cee0817039b87..1b3ea760506ba5615033b3b0cd68392c7c827853 100644 --- a/core/modules/file/lib/Drupal/file/Tests/UsageTest.php +++ b/core/modules/file/lib/Drupal/file/Tests/UsageTest.php @@ -123,24 +123,24 @@ function testRemoveUsage() { } /** - * Ensure that temporary files are removed. + * Create files for all the possible combinations of age and status. * - * Create files for all the possible combinations of age and status. We are - * using UPDATE statements because using the API would set the timestamp. + * We are using UPDATE statements because using the API would set the + * timestamp. */ - function testTempFileCleanup() { - // Temporary file that is older than DRUPAL_MAXIMUM_TEMP_FILE_AGE. + function createTempFiles() { + // Temporary file that is old. $temp_old = file_save_data(''); db_update('file_managed') ->fields(array( 'status' => 0, - 'changed' => 1, + 'changed' => REQUEST_TIME - $this->container->get('config.factory')->get('system.file')->get('temporary_maximum_age') - 1, )) ->condition('fid', $temp_old->id()) ->execute(); $this->assertTrue(file_exists($temp_old->getFileUri()), 'Old temp file was created correctly.'); - // Temporary file that is less than DRUPAL_MAXIMUM_TEMP_FILE_AGE. + // Temporary file that is new. $temp_new = file_save_data(''); db_update('file_managed') ->fields(array('status' => 0)) @@ -148,17 +148,25 @@ function testTempFileCleanup() { ->execute(); $this->assertTrue(file_exists($temp_new->getFileUri()), 'New temp file was created correctly.'); - // Permanent file that is older than DRUPAL_MAXIMUM_TEMP_FILE_AGE. + // Permanent file that is old. $perm_old = file_save_data(''); db_update('file_managed') - ->fields(array('changed' => 1)) + ->fields(array('changed' => REQUEST_TIME - $this->container->get('config.factory')->get('system.file')->get('temporary_maximum_age') - 1)) ->condition('fid', $temp_old->id()) ->execute(); $this->assertTrue(file_exists($perm_old->getFileUri()), 'Old permanent file was created correctly.'); - // Permanent file that is newer than DRUPAL_MAXIMUM_TEMP_FILE_AGE. + // Permanent file that is new. $perm_new = file_save_data(''); $this->assertTrue(file_exists($perm_new->getFileUri()), 'New permanent file was created correctly.'); + return array($temp_old, $temp_new, $perm_old, $perm_new); + } + + /** + * Ensure that temporary files are removed by default. + */ + function testTempFileCleanupDefault() { + list($temp_old, $temp_new, $perm_old, $perm_new) = $this->createTempFiles(); // Run cron and then ensure that only the old, temp file was deleted. $this->container->get('cron')->run(); @@ -167,4 +175,42 @@ function testTempFileCleanup() { $this->assertTrue(file_exists($perm_old->getFileUri()), 'Old permanent file was correctly ignored.'); $this->assertTrue(file_exists($perm_new->getFileUri()), 'New permanent file was correctly ignored.'); } + + /** + * Ensure that temporary files are kept as configured. + */ + function testTempFileNoCleanup() { + list($temp_old, $temp_new, $perm_old, $perm_new) = $this->createTempFiles(); + + // Set the max age to 0, meaning no temporary files will be deleted. + \Drupal::config('system.file') + ->set('temporary_maximum_age', 0) + ->save(); + + // Run cron and then ensure that no file was deleted. + $this->container->get('cron')->run(); + $this->assertTrue(file_exists($temp_old->getFileUri()), 'Old temp file was correctly ignored.'); + $this->assertTrue(file_exists($temp_new->getFileUri()), 'New temp file was correctly ignored.'); + $this->assertTrue(file_exists($perm_old->getFileUri()), 'Old permanent file was correctly ignored.'); + $this->assertTrue(file_exists($perm_new->getFileUri()), 'New permanent file was correctly ignored.'); + } + + /** + * Ensure that temporary files are kept as configured. + */ + function testTempFileCustomCleanup() { + list($temp_old, $temp_new, $perm_old, $perm_new) = $this->createTempFiles(); + + // Set the max age to older than default. + \Drupal::config('system.file') + ->set('temporary_maximum_age', 21600 + 2) + ->save(); + + // Run cron and then ensure that more files were deleted. + $this->container->get('cron')->run(); + $this->assertTrue(file_exists($temp_old->getFileUri()), 'Old temp file was correctly ignored.'); + $this->assertTrue(file_exists($temp_new->getFileUri()), 'New temp file was correctly ignored.'); + $this->assertTrue(file_exists($perm_old->getFileUri()), 'Old permanent file was correctly ignored.'); + $this->assertTrue(file_exists($perm_new->getFileUri()), 'New permanent file was correctly ignored.'); + } } diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index 91f06055590dd07f066868fb19f2975bd480a0ff..2d9d11df9effc37fcbaedf2362da6abf95d6b750 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -355,6 +355,9 @@ system.file: temporary: type: string label: 'Temporary directory' + temporary_maximum_age: + type: integer + label: 'Maximum age for temporary files' system.image: type: mapping diff --git a/core/modules/system/config/system.file.yml b/core/modules/system/config/system.file.yml index 35ff1ec4e42eb23b819865859dc8692094c4d3ff..e9f9df0e6f9d5e4cfa1a9bf5f7d8b821f6ddf890 100644 --- a/core/modules/system/config/system.file.yml +++ b/core/modules/system/config/system.file.yml @@ -3,3 +3,4 @@ default_scheme: 'public' path: private: '' temporary: '' +temporary_maximum_age: 21600 diff --git a/core/modules/system/lib/Drupal/system/Form/FileSystemForm.php b/core/modules/system/lib/Drupal/system/Form/FileSystemForm.php index 2d516846a86a4f5864c6ac50cdc0d2d4ea583bbf..4f5a4ee773b7177e1edeb4892dc66159b8cbc126 100644 --- a/core/modules/system/lib/Drupal/system/Form/FileSystemForm.php +++ b/core/modules/system/lib/Drupal/system/Form/FileSystemForm.php @@ -68,6 +68,17 @@ public function buildForm(array $form, array &$form_state) { ); } + $intervals = array(0, 21600, 43200, 86400, 604800, 2419200, 7776000); + $period = array_combine($intervals, array_map('format_interval', $intervals)); + $period[0] = t('Never'); + $form['temporary_maximum_age'] = array( + '#type' => 'select', + '#title' => t('Delete orphaned files after'), + '#default_value' => $config->get('temporary_maximum_age'), + '#options' => $period, + '#description' => t('Orphaned files are not referenced from any content but remain in the file system and may appear in administrative listings. Warning: If enabled, orphaned files will be permanently deleted and may not be recoverable.'), + ); + return parent::buildForm($form, $form_state); } @@ -77,7 +88,8 @@ public function buildForm(array $form, array &$form_state) { public function submitForm(array &$form, array &$form_state) { $config = $this->configFactory->get('system.file') ->set('path.private', $form_state['values']['file_private_path']) - ->set('path.temporary', $form_state['values']['file_temporary_path']); + ->set('path.temporary', $form_state['values']['file_temporary_path']) + ->set('temporary_maximum_age', $form_state['values']['temporary_maximum_age']); if (isset($form_state['values']['file_default_scheme'])) { $config->set('default_scheme', $form_state['values']['file_default_scheme']); diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 541168da5b50e579453ecef55c7f93fd606b1286..d719b5a8a7b2c1698805e1431c07b0d2256454ae 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -14,11 +14,6 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use GuzzleHttp\Exception\RequestException; -/** - * Maximum age of temporary files in seconds. - */ -const DRUPAL_MAXIMUM_TEMP_FILE_AGE = 21600; - /** * New users will be set to the default time zone at registration. */ diff --git a/core/modules/user/lib/Drupal/user/Tests/UserPictureTest.php b/core/modules/user/lib/Drupal/user/Tests/UserPictureTest.php index 5ed7462dbd14a05e8fbec9005b425538bea16efd..335050d30c6d6a41eef8d9a367521ad17bfa0971 100644 --- a/core/modules/user/lib/Drupal/user/Tests/UserPictureTest.php +++ b/core/modules/user/lib/Drupal/user/Tests/UserPictureTest.php @@ -65,11 +65,12 @@ function testCreateDeletePicture() { $this->drupalPostForm('user/' . $this->web_user->id() . '/edit', $edit, t('Remove')); $this->drupalPostForm(NULL, array(), t('Save')); - // Call system_cron() to clean up the file. Make sure the timestamp - // of the file is older than DRUPAL_MAXIMUM_TEMP_FILE_AGE. + // Call file_cron() to clean up the file. Make sure the timestamp + // of the file is older than the system.file.temporary_maximum_age + // configuration value. db_update('file_managed') ->fields(array( - 'changed' => REQUEST_TIME - (DRUPAL_MAXIMUM_TEMP_FILE_AGE + 1), + 'changed' => REQUEST_TIME - ($this->container->get('config.factory')->get('system.file')->get('temporary_maximum_age') + 1), )) ->condition('fid', $file->id()) ->execute();