diff --git a/core/modules/file/config/install/file.settings.yml b/core/modules/file/config/install/file.settings.yml index e652277..9191a0b 100644 --- a/core/modules/file/config/install/file.settings.yml +++ b/core/modules/file/config/install/file.settings.yml @@ -3,4 +3,4 @@ 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..42d57d9 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 d371384..ca08324 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,34 @@ function file_requirements($phase) { 'value' => $value, 'description' => $description, ); + $file_config = \Drupal::configFactory()->get('file.settings'); + $requirements['file_orphaned_file_delete'] = array( + '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.', array(':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.', array(':url' => Url::fromRoute('system.file_system_settings', [], ['fragment' => 'edit-make-unused-managed-files-temporary'])->toString())); +} + +/** + * @} End of "addtogroup updates-8.3.0". + */ diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 687a62c..4d47631 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(); +} 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 12647b1..44e88da 100644 --- a/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php +++ b/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php @@ -2,6 +2,7 @@ namespace Drupal\file\FileUsage; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Database\Connection; use Drupal\file\FileInterface; @@ -33,9 +34,11 @@ 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..2754765 100644 --- a/core/modules/file/src/FileUsage/FileUsageBase.php +++ b/core/modules/file/src/FileUsage/FileUsageBase.php @@ -2,6 +2,7 @@ namespace Drupal\file\FileUsage; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\file\FileInterface; /** @@ -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,11 @@ 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..f72dd09 --- /dev/null +++ b/core/modules/file/src/Tests/FileAdminTest.php @@ -0,0 +1,31 @@ +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.'); + } + +} diff --git a/core/modules/file/src/Tests/FileFieldRevisionTest.php b/core/modules/file/src/Tests/FileFieldRevisionTest.php index f5d00c7..032c58b 100644 --- a/core/modules/file/src/Tests/FileFieldRevisionTest.php +++ b/core/modules/file/src/Tests/FileFieldRevisionTest.php @@ -22,6 +22,10 @@ class FileFieldRevisionTest extends FileFieldTestBase { * should be deleted also. */ 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 708f138..a481732 100644 --- a/core/modules/file/src/Tests/FileListingTest.php +++ b/core/modules/file/src/Tests/FileListingTest.php @@ -30,6 +30,9 @@ class FileListingTest extends FileFieldTestBase { 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(); + $this->adminUser = $this->drupalCreateUser(array('access files overview', 'bypass node access')); $this->baseUser = $this->drupalCreateUser(); $this->createFileField('file', 'node', 'article', array(), array('file_extensions' => 'txt png')); diff --git a/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php b/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php index 7987bcf..03a4b10 100644 --- a/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php +++ b/core/modules/file/src/Tests/FileOnTranslatedEntityTest.php @@ -29,6 +29,9 @@ 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 2705ef2..86027f2 100644 --- a/core/modules/file/src/Tests/FilePrivateTest.php +++ b/core/modules/file/src/Tests/FilePrivateTest.php @@ -26,6 +26,8 @@ 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 b880571..bfc8e22 100644 --- a/core/modules/file/tests/src/Kernel/DeleteTest.php +++ b/core/modules/file/tests/src/Kernel/DeleteTest.php @@ -28,6 +28,10 @@ function testUnused() { * Tries deleting a file that is in use. */ 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 8e26015..55fb755 100644 --- a/core/modules/file/tests/src/Kernel/UsageTest.php +++ b/core/modules/file/tests/src/Kernel/UsageTest.php @@ -75,10 +75,29 @@ 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(). */ - function testRemoveUsage() { + function doTestRemoveUsage() { $file = $this->createFile(); + $file->setPermanent(); $file_usage = $this->container->get('file.usage'); db_insert('file_usage') ->fields(array( @@ -116,6 +135,7 @@ 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 a8985ce..7001b2c 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 9271bf4..f35f13a 100644 --- a/core/modules/system/src/Form/FileSystemForm.php +++ b/core/modules/system/src/Form/FileSystemForm.php @@ -125,10 +125,10 @@ public function buildForm(array $form, FormStateInterface $form_state) { $period[0] = t('Never'); $form['temporary_maximum_age'] = array( '#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.'), + '#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.'), ); return parent::buildForm($form, $form_state); diff --git a/core/modules/user/src/Tests/UserPictureTest.php b/core/modules/user/src/Tests/UserPictureTest.php index 3f8db42..75edbfd 100644 --- a/core/modules/user/src/Tests/UserPictureTest.php +++ b/core/modules/user/src/Tests/UserPictureTest.php @@ -33,6 +33,10 @@ 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(array( 'access content', 'access comments',