diff --git a/core/modules/file/config/install/file.settings.yml b/core/modules/file/config/install/file.settings.yml index e652277..1b2e70a 100644 --- a/core/modules/file/config/install/file.settings.yml +++ b/core/modules/file/config/install/file.settings.yml @@ -3,4 +3,5 @@ description: length: 128 icon: directory: 'core/modules/file/icons' + make_unused_managed_files_temporary: false diff --git a/core/modules/file/config/schema/file.schema.yml b/core/modules/file/config/schema/file.schema.yml index b9f8918..683234e 100644 --- a/core/modules/file/config/schema/file.schema.yml +++ b/core/modules/file/config/schema/file.schema.yml @@ -21,6 +21,9 @@ file.settings: directory: type: path label: 'Directory' + make_unused_managed_files_temporary: + type: boolean + label: 'Controls if unused files should be marked temporary' field.storage_settings.file: type: base_entity_reference_field_settings diff --git a/core/modules/file/file.install b/core/modules/file/file.install index 9134a25..d506faa 100644 --- a/core/modules/file/file.install +++ b/core/modules/file/file.install @@ -5,6 +5,8 @@ * Install, update and uninstall functions for File module. */ +use Drupal\Core\Url; + /** * Implements hook_schema(). */ @@ -112,7 +114,40 @@ function file_requirements($phase) { 'value' => $value, 'description' => $description, ]; + $file_config = \Drupal::configFactory()->get('file.settings'); + $requirements['file_orphaned_file_delete'] = [ + 'title' => t('Orphaned file delete'), + 'value' => t('There are currently known bugs with file usage counting. It is recommended to leave \'Schedule all unused files for deletion\' disabled to prevent the loss of files.', [ + ':url' => Url::fromRoute('system.file_system_settings', [], ['fragment' => 'edit-make-unused-managed-files-temporary']) + ->toString() + ]), + 'severity' => $file_config->get('make_unused_managed_files_temporary') ? REQUIREMENT_WARNING : REQUIREMENT_OK, + ]; } return $requirements; } + +/** + * @addtogroup updates-8.3.0 + * @{ + */ + +/** + * Prevent unused files from being deleted. + */ +function file_update_8300() { + // Disable deletion of temporary files. + \Drupal::configFactory()->getEditable('file.settings') + ->set('make_unused_managed_files_temporary', FALSE) + ->save(); + + return t('Files that have no remaining usages are no longer deleted by default. It is recommended to leave \'Schedule all unused files for deletion\' disabled to prevent the loss of files.', [ + ':url' => Url::fromRoute('system.file_system_settings', [], ['fragment' => 'edit-make-unused-managed-files-temporary']) + ->toString() + ]); +} + +/** + * @} End of "addtogroup updates-8.3.0". + */ \ No newline at end of file diff --git a/core/modules/file/file.module b/core/modules/file/file.module index a6fc3f6..ac0e0c2 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -1560,3 +1560,41 @@ function _views_file_status($choice = NULL) { return $status; } + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function file_form_system_file_system_settings_alter(&$form, FormStateInterface $form_state) { + $config = \Drupal::configFactory()->getEditable('file.settings'); + + $form['temporary_files'] = [ + '#type' => 'details', + '#title' => t('Temporary and other unused files'), + '#open' => TRUE + ]; + + // Move the temporary_maximum_age settings. + $temporary_maximum_age_element = $form['temporary_maximum_age']; + unset($form['temporary_maximum_age']); + $form['temporary_files']['temporary_maximum_age'] = $temporary_maximum_age_element; + + $form['temporary_files']['make_unused_managed_files_temporary'] = [ + '#type' => 'checkbox', + '#title' => t('Schedule all unused files for deletion'), + '#default_value' => $config->get('make_unused_managed_files_temporary'), + '#description' => t('If enabled, all files that are not referenced will be deleted after the time set above. For example, enabling this will delete files that previously were used by deleted content. Warning: There are currently known bugs with file usage counting. It is recommended to leave disabled to prevent the loss of files.'), + ]; + + $form['#submit'][] = 'file_system_file_settings_submit'; +} + +/** + * Form submission handler for system_logging_settings(). + * + * @see syslog_form_system_logging_settings_alter() + */ +function file_system_file_settings_submit($form, FormStateInterface $form_state) { + \Drupal::configFactory()->getEditable('file.settings') + ->set('make_unused_managed_files_temporary', $form_state->getValue('make_unused_managed_files_temporary')) + ->save(); +} \ No newline at end of file diff --git a/core/modules/file/file.services.yml b/core/modules/file/file.services.yml index 1c463af..b4b2418 100644 --- a/core/modules/file/file.services.yml +++ b/core/modules/file/file.services.yml @@ -1,6 +1,6 @@ services: file.usage: class: Drupal\file\FileUsage\DatabaseFileUsageBackend - arguments: ['@database'] + arguments: ['@database', '@config.factory'] tags: - { name: backend_overridable } diff --git a/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php b/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php index cfb0c44..4b47bed 100644 --- a/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php +++ b/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php @@ -3,6 +3,7 @@ namespace Drupal\file\FileUsage; use Drupal\Core\Database\Connection; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\file\FileInterface; /** @@ -33,7 +34,8 @@ class DatabaseFileUsageBackend extends FileUsageBase { * @param string $table * (optional) The table to store file usage info. Defaults to 'file_usage'. */ - public function __construct(Connection $connection, $table = 'file_usage') { + public function __construct(Connection $connection, ConfigFactoryInterface $config_factory = NULL, $table = 'file_usage') { + parent::__construct($config_factory); $this->connection = $connection; $this->tableName = $table; diff --git a/core/modules/file/src/FileUsage/FileUsageBase.php b/core/modules/file/src/FileUsage/FileUsageBase.php index c90359b..959bb4c 100644 --- a/core/modules/file/src/FileUsage/FileUsageBase.php +++ b/core/modules/file/src/FileUsage/FileUsageBase.php @@ -3,6 +3,7 @@ namespace Drupal\file\FileUsage; use Drupal\file\FileInterface; +use Drupal\Core\Config\ConfigFactoryInterface; /** * Defines the base class for database file usage backend. @@ -10,6 +11,27 @@ abstract class FileUsageBase implements FileUsageInterface { /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * Creates a FileUsageBase object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * (optional) The config factory. Defaults to NULL and will use + * \Drupal::configFactory() instead. + * + * @deprecated The $config_factory parameter will become required in Drupal + * 9.0.0. + */ + public function __construct(ConfigFactoryInterface $config_factory = NULL) { + $this->configFactory = $config_factory ?: \Drupal::configFactory(); + } + + /** * {@inheritdoc} */ public function add(FileInterface $file, $module, $type, $id, $count = 1) { @@ -24,6 +46,10 @@ public function add(FileInterface $file, $module, $type, $id, $count = 1) { * {@inheritdoc} */ public function delete(FileInterface $file, $module, $type = NULL, $id = NULL, $count = 1) { + // Do not actually mark files as temporary when the behavior is disabled. + if (!$this->configFactory->get('file.settings')->get('make_unused_managed_files_temporary')) { + return; + } // If there are no more remaining usages of this file, mark it as temporary, // which result in a delete through system_cron(). $usage = \Drupal::service('file.usage')->listUsage($file); diff --git a/core/modules/file/src/Tests/FileAdminTest.php b/core/modules/file/src/Tests/FileAdminTest.php new file mode 100644 index 0000000..c3d5e2d --- /dev/null +++ b/core/modules/file/src/Tests/FileAdminTest.php @@ -0,0 +1,33 @@ +drupalLogin($this->drupalCreateUser(['administer site configuration'])); + $this->assertFalse($this->config('file.settings') + ->get('make_unused_managed_files_temporary'), 'The file.settings:make_unused_managed_files_temporary is set to FALSE.'); + $this->drupalPostForm('admin/config/media/file-system', ['make_unused_managed_files_temporary' => TRUE], t('Save configuration')); + $this->assertTrue($this->config('file.settings') + ->get('make_unused_managed_files_temporary'), 'The file.settings:make_unused_managed_files_temporary has been set to TRUE.'); + } + +} \ No newline at end of file diff --git a/core/modules/file/src/Tests/FileFieldRevisionTest.php b/core/modules/file/src/Tests/FileFieldRevisionTest.php index 7de496f..6f139bf 100644 --- a/core/modules/file/src/Tests/FileFieldRevisionTest.php +++ b/core/modules/file/src/Tests/FileFieldRevisionTest.php @@ -22,6 +22,11 @@ class FileFieldRevisionTest extends FileFieldTestBase { * should be deleted also. */ public function testRevisions() { + // This test expects unused managed files to be marked as a temporary file + // and then deleted up by file_cron(). + $this->config('file.settings') + ->set('make_unused_managed_files_temporary', TRUE) + ->save(); $node_storage = $this->container->get('entity.manager')->getStorage('node'); $type_name = 'article'; $field_name = strtolower($this->randomMachineName()); diff --git a/core/modules/file/src/Tests/FileListingTest.php b/core/modules/file/src/Tests/FileListingTest.php index e348949..e1e8ed7 100644 --- a/core/modules/file/src/Tests/FileListingTest.php +++ b/core/modules/file/src/Tests/FileListingTest.php @@ -33,6 +33,10 @@ protected function setUp() { $this->adminUser = $this->drupalCreateUser(['access files overview', 'bypass node access']); $this->baseUser = $this->drupalCreateUser(); $this->createFileField('file', 'node', 'article', [], ['file_extensions' => 'txt png']); + // This test expects unused managed files to be marked as a temporary file. + $this->config('file.settings') + ->set('make_unused_managed_files_temporary', TRUE) + ->save(); } /** diff --git a/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php b/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php index 5f1e707..b8a22ae 100644 --- a/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php +++ b/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php @@ -29,6 +29,11 @@ class FileOnTranslatedEntityTest extends FileFieldTestBase { protected function setUp() { parent::setUp(); + // This test expects unused managed files to be marked as temporary a file. + $this->config('file.settings') + ->set('make_unused_managed_files_temporary', TRUE) + ->save(); + // Create the "Basic page" node type. // @todo Remove the disabling of new revision creation in // https://www.drupal.org/node/1239558. diff --git a/core/modules/file/src/Tests/FilePrivateTest.php b/core/modules/file/src/Tests/FilePrivateTest.php index 3c9776f..f4038c3 100644 --- a/core/modules/file/src/Tests/FilePrivateTest.php +++ b/core/modules/file/src/Tests/FilePrivateTest.php @@ -26,6 +26,10 @@ protected function setUp() { node_access_test_add_field(NodeType::load('article')); node_access_rebuild(); \Drupal::state()->set('node_access_test.private', TRUE); + // This test expects unused managed files to be marked as a temporary file. + $this->config('file.settings') + ->set('make_unused_managed_files_temporary', TRUE) + ->save(); } /** diff --git a/core/modules/file/tests/src/Kernel/DeleteTest.php b/core/modules/file/tests/src/Kernel/DeleteTest.php index 3b868c0..de5a5bc 100644 --- a/core/modules/file/tests/src/Kernel/DeleteTest.php +++ b/core/modules/file/tests/src/Kernel/DeleteTest.php @@ -28,6 +28,11 @@ public function testUnused() { * Tries deleting a file that is in use. */ public function testInUse() { + // This test expects unused managed files to be marked as a temporary file + // and then deleted up by file_cron(). + $this->config('file.settings') + ->set('make_unused_managed_files_temporary', TRUE) + ->save(); $file = $this->createFile(); $file_usage = $this->container->get('file.usage'); $file_usage->add($file, 'testing', 'test', 1); diff --git a/core/modules/file/tests/src/Kernel/UsageTest.php b/core/modules/file/tests/src/Kernel/UsageTest.php index 672de0f..88812a1 100644 --- a/core/modules/file/tests/src/Kernel/UsageTest.php +++ b/core/modules/file/tests/src/Kernel/UsageTest.php @@ -75,10 +75,33 @@ public function testAddUsage() { } /** + * Tests file usage deletion when files are made temporary. + */ + function testRemoveUsageTemporary() { + $this->config('file.settings') + ->set('make_unused_managed_files_temporary', TRUE) + ->save(); + $file = $this->doTestRemoveUsage(); + $this->assertTrue($file->isTemporary()); + } + + /** + * Tests file usage deletion when files are made temporary. + */ + function testRemoveUsageNonTemporary() { + $this->config('file.settings') + ->set('make_unused_managed_files_temporary', FALSE) + ->save(); + $file = $this->doTestRemoveUsage(); + $this->assertFalse($file->isTemporary()); + } + + /** * Tests \Drupal\file\FileUsage\DatabaseFileUsageBackend::delete(). */ - public function testRemoveUsage() { + public function doTestRemoveUsage() { $file = $this->createFile(); + $file->setPermanent(); $file_usage = $this->container->get('file.usage'); db_insert('file_usage') ->fields([ @@ -116,6 +139,7 @@ public function testRemoveUsage() { ->execute() ->fetchField(); $this->assertIdentical(FALSE, $count, 'Decrementing non-exist record complete.'); + return $file; } /** diff --git a/core/modules/image/src/Tests/ImageOnTranslatedEntityTest.php b/core/modules/image/src/Tests/ImageOnTranslatedEntityTest.php index e1f70d7..c5f16cf 100644 --- a/core/modules/image/src/Tests/ImageOnTranslatedEntityTest.php +++ b/core/modules/image/src/Tests/ImageOnTranslatedEntityTest.php @@ -29,6 +29,9 @@ class ImageOnTranslatedEntityTest extends ImageFieldTestBase { protected function setUp() { parent::setUp(); + // This test expects unused managed files to be marked as a temporary file. + $this->config('file.settings')->set('make_unused_managed_files_temporary', TRUE)->save(); + // Create the "Basic page" node type. // @todo Remove the disabling of new revision creation in // https://www.drupal.org/node/1239558. diff --git a/core/modules/system/src/Form/FileSystemForm.php b/core/modules/system/src/Form/FileSystemForm.php index ff0b792..ac70aab 100644 --- a/core/modules/system/src/Form/FileSystemForm.php +++ b/core/modules/system/src/Form/FileSystemForm.php @@ -116,7 +116,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#title' => t('Default download method'), '#default_value' => $config->get('default_scheme'), '#options' => $options, - '#description' => t('This setting is used as the preferred download method. The use of public files is more efficient, but does not provide any access control.'), + '#description' => t('Temporary files are not referenced, but are in the file system and therefore may show up in administrative lists. Warning: If enabled, temporary files will be permanently deleted and may not be recoverable.'), ]; } @@ -125,7 +125,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $period[0] = t('Never'); $form['temporary_maximum_age'] = [ '#type' => 'select', - '#title' => t('Delete orphaned files after'), + '#title' => t('Delete temporary 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.'), diff --git a/core/modules/user/src/Tests/UserPictureTest.php b/core/modules/user/src/Tests/UserPictureTest.php index 6d546f0..d850413 100644 --- a/core/modules/user/src/Tests/UserPictureTest.php +++ b/core/modules/user/src/Tests/UserPictureTest.php @@ -33,6 +33,12 @@ class UserPictureTest extends WebTestBase { protected function setUp() { parent::setUp(); + // This test expects unused managed files to be marked temporary and then + // cleaned up by file_cron(). + $this->config('file.settings') + ->set('make_unused_managed_files_temporary', TRUE) + ->save(); + $this->webUser = $this->drupalCreateUser([ 'access content', 'access comments',