File not marked temporary and usage not updated if only used in past revisions when node is deleted

Created on 13 March 2024, 8 months ago
Updated 21 March 2024, 8 months ago

Problem/Motivation

If a file is used by a node's revisions, but not the current revision, the file is never marked temporary or removed when the node is deleted.

Steps to reproduce

  1. Set $config['file.settings']['make_unused_managed_files_temporary'] = TRUE; in settings.php
  2. Create a content type with a file field.
  3. Create a new node of that type and add a file to it.
  4. Save the node.
  5. Edit the node and remove the file.
  6. Save the node. (Now the file is only used by old revisions.)
  7. Delete the node.
  8. Clear the cache.
  9. Access '/admin/content/files', and the status of the file is 'Permanent', 'USED IN' of the file is not 0.

Expectation: the file status should be Temporary, and the "Used in" column for the file should be 0.

Note: When deleting individual revisions of a node with a file on /node/NID/revisions, the usage of the file does get updated properly, and upon deleting the last revision that used the file, the file will be marked Temporary.

Proposed resolution

This could be added to file.module. This is a generic solution that works for me.

Replace MODULE with the actual module name in both the function name and in the line $file_usage->delete($file, 'MODULE', $entity->getEntityTypeId(), $entity->id(), 0);. Note that if you use this as a workaround, either apply a patch with Composer or use a custom module, don't modify core/contrib module files manually.

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\RevisionableStorageInterface;
use Drupal\file\Entity\File;
use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
use Drupal\file\Plugin\Field\FieldType\FileItem;

/**
 * Implements hook_entity_predelete().
 */
function MODULE_entity_predelete(EntityInterface $entity) {
  $entity_type_manager = \Drupal::entityTypeManager();
  $entity_type_id = $entity->getEntityTypeId();
  $entity_type = $entity_type_manager->getDefinition($entity_type_id);

  // Check if the entity type supports revisions and has fields.
  if (!$entity_type->isRevisionable() || !($entity instanceof FieldableEntityInterface)) {
    return;
  }

  // Get the fields that have files.
  $file_fields = [];
  foreach ($entity->getFields() as $field_name => $field_list) {
    if ($field_list instanceof FileFieldItemList) {
      $file_fields[$field_name] = $field_name;
      continue;
    }

    // Not sure if checking the field items is necessary.
    $field_item = $field_list->first();
    if ($field_item instanceof FileItem) {
      $file_fields[$field_name] = $field_name;
    }
  }

  if (count($file_fields) === 0) {
    return;
  }

  $entity_storage = $entity_type_manager->getStorage($entity_type_id);
  if ($entity_storage instanceof RevisionableStorageInterface) {
    // Load all revision IDs of the entity.
    $revision_ids = $entity_storage
      ->getQuery()
      ->allRevisions()
      ->condition($entity_type->getKey('id'), $entity->id())
      ->accessCheck(FALSE)
      ->execute();

    // Loop through each revision and get the referenced files.
    $fids = [];
    foreach (array_keys($revision_ids) as $revision_id) {
      /** @var \Drupal\Core\Entity\FieldableEntityInterface&\Drupal\Core\Entity\RevisionableInterface|null $revision */
      $revision = $entity_storage->loadRevision($revision_id);
      if ($revision === NULL) {
        continue;
      }

      $revision_fields = $revision->getFields();

      // Iterate over all file fields of the entity getting all file IDs.
      foreach ($file_fields as $field_name) {
        // Iterate over all values of the field.
        foreach ($revision_fields[$field_name] as $field) {
          // Ensure the field is an instance of FileItem.
          if ($field instanceof FileItem) {
            if ($field->target_id !== NULL) {
              $fids[$field->target_id] = $field->target_id;
            }
          }
        }
      }
    }
  }

  // Delete the file usage for this entity's files for all revisions.
  if ($fids) {
    // This could be done in chunks, if desired.
    $files = File::loadMultiple($fids);

    /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */
    $file_usage = \Drupal::service('file.usage');
    foreach ($files as $file) {
      $file_usage->delete($file, 'MODULE', $entity->getEntityTypeId(), $entity->id(), 0);
    }
  }

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

🐛 Bug report
Status

Active

Version

11.0 🔥

Component
File module 

Last updated about 12 hours ago

Created by

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Production build 0.71.5 2024