Commit 87434339 authored by Samuel Mortenson's avatar Samuel Mortenson Committed by Sam Mortenson
Browse files

Issue #3258513 by samuel.mortenson: Add a user interface for tome:clean-files

parent 3b5ebbe1
Loading
Loading
Loading
Loading
+54 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\tome_sync;

use Drupal\Core\StreamWrapper\StreamWrapperManager;

/**
 * Contains shared methods for cleaning files.
 */
trait CleanFilesTrait {

  /**
   * Assembles a list of files that are unused.
   *
   * @return array
   *   An associative array mapping file UUIDs to their URIs.
   */
  protected function getUnusedFiles() {
    $files = [];
    $names = $this->contentStorage->listAll('file.');
    foreach ($names as $name) {
      $data = $this->contentStorage->read($name);
      list(, $uuid) = TomeSyncHelper::getPartsFromContentName($name);
      $files[$uuid] = StreamWrapperManager::getTarget($data['uri'][0]['value']);
    }
    $callback = function ($value) use (&$files) {
      if (is_string($value)) {
        foreach ($files as $uuid => $filename) {
          if (strpos($value, $uuid) !== FALSE || strpos($value, $filename) !== FALSE) {
            unset($files[$uuid]);
          }
        }
      }
    };
    $names = array_diff($this->contentStorage->listAll(), $names);
    foreach ($names as $name) {
      if (!$files) {
        break;
      }
      $data = $this->contentStorage->read($name);
      array_walk_recursive($data, $callback);
    }
    $names = $this->configStorage->listAll();
    foreach ($names as $name) {
      if (!$files) {
        break;
      }
      $data = $this->configStorage->read($name);
      array_walk_recursive($data, $callback);
    }
    return $files;
  }

}
+7 −45
Original line number Diff line number Diff line
@@ -4,13 +4,13 @@ namespace Drupal\tome_sync\Commands;

use Drupal\tome_base\PathTrait;
use Drupal\tome_base\CommandBase;
use Drupal\tome_sync\TomeSyncHelper;
use Drupal\tome_sync\FileSyncInterface;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\tome_sync\CleanFilesTrait;
use Drupal\tome_sync\ContentIndexerTrait;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManager;

/**
 * Contains the tome:clean-files command.
@@ -21,6 +21,7 @@ class CleanFilesCommand extends CommandBase {

  use PathTrait;
  use ContentIndexerTrait;
  use CleanFilesTrait;

  /**
   * The target content storage.
@@ -59,12 +60,15 @@ class CleanFilesCommand extends CommandBase {
   *   The target config storage.
   * @param \Drupal\tome_sync\FileSyncInterface $file_sync
   *   The file sync service.
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   *   The file system.
   */
  public function __construct(StorageInterface $content_storage, StorageInterface $config_storage, FileSyncInterface $file_sync) {
  public function __construct(StorageInterface $content_storage, StorageInterface $config_storage, FileSyncInterface $file_sync, FileSystemInterface $file_system) {
    parent::__construct();
    $this->contentStorage = $content_storage;
    $this->configStorage = $config_storage;
    $this->fileSync = $file_sync;
    $this->fileSystem = $file_system;
  }

  /**
@@ -97,46 +101,4 @@ class CleanFilesCommand extends CommandBase {
    $this->io()->success('Deleted all unused files.');
  }

  /**
   * Assembles a list of files that should be unused.
   *
   * @return array
   *   An associative array mapping file UUIDs to their URIs.
   */
  protected function getUnusedFiles() {
    $files = [];
    $names = $this->contentStorage->listAll('file.');
    foreach ($names as $name) {
      $data = $this->contentStorage->read($name);
      list(, $uuid) = TomeSyncHelper::getPartsFromContentName($name);
      $files[$uuid] = StreamWrapperManager::getTarget($data['uri'][0]['value']);
    }
    $callback = function ($value) use (&$files) {
      if (is_string($value)) {
        foreach ($files as $uuid => $filename) {
          if (strpos($value, $uuid) !== FALSE || strpos($value, $filename) !== FALSE) {
            unset($files[$uuid]);
          }
        }
      }
    };
    $names = array_diff($this->contentStorage->listAll(), $names);
    foreach ($names as $name) {
      if (!$files) {
        break;
      }
      $data = $this->contentStorage->read($name);
      array_walk_recursive($data, $callback);
    }
    $names = $this->configStorage->listAll();
    foreach ($names as $name) {
      if (!$files) {
        break;
      }
      $data = $this->configStorage->read($name);
      array_walk_recursive($data, $callback);
    }
    return $files;
  }

}
+148 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\tome_sync\Form;

use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\tome_base\PathTrait;
use Drupal\tome_sync\CleanFilesTrait;
use Drupal\tome_sync\ContentIndexerTrait;
use Drupal\tome_sync\FileSyncInterface;

/**
 * Contains a form for deleting unused files.
 *
 * @internal
 */
class CleanFilesForm extends FormBase {

  use PathTrait;
  use ContentIndexerTrait;
  use CleanFilesTrait;

  /**
   * The target content storage.
   *
   * @var \Drupal\Core\Config\StorageInterface
   */
  protected $contentStorage;

  /**
   * The config storage.
   *
   * @var \Drupal\Core\Config\StorageInterface
   */
  protected $configStorage;

  /**
   * The file system.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * The file sync service.
   *
   * @var \Drupal\tome_sync\FileSyncInterface
   */
  protected $fileSync;

  /**
   * Creates a CleanFilesForm object.
   *
   * @param \Drupal\Core\Config\StorageInterface $content_storage
   *   The target content storage.
   * @param \Drupal\Core\Config\StorageInterface $config_storage
   *   The target config storage.
   * @param \Drupal\tome_sync\FileSyncInterface $file_sync
   *   The file sync service.
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   *   The file system.
   */
  public function __construct(StorageInterface $content_storage, StorageInterface $config_storage, FileSyncInterface $file_sync, FileSystemInterface $file_system) {
    $this->contentStorage = $content_storage;
    $this->configStorage = $config_storage;
    $this->fileSync = $file_sync;
    $this->fileSystem = $file_system;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('tome_sync.storage.content'),
      $container->get('config.storage.sync'),
      $container->get('tome_sync.file_sync'),
      $container->get('file_system'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'tome_sync_clean_files_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $files = $this->getUnusedFiles();
    if (empty($files)) {
      \Drupal::messenger()->addStatus('No unused files found!');
      return $form;
    }

    $form['warning'] = [
      '#theme' => 'status_messages',
      '#message_list' => [
        'warning' => [t('The files below appear to be unused and will be deleted. This operation is permanent.')],
      ],
      '#status_headings' => ['warning' => t('Review list below before proceeding')],
    ];

    $form['file_list'] = [
      '#type' => 'html_tag',
      '#tag' => 'ul',
    ];
    asort($files);
    foreach ($files as $uuid => $filename) {
      $form['file_list'][] = [
        '#type' => 'html_tag',
        '#tag' => 'li',
        '#value' => "$filename ($uuid)",
      ];
    }

    $form['actions'] = [
      '#type' => 'actions',
    ];

    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => t('Delete @count files above', ['@count' => count($files)]),
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $files = $this->getUnusedFiles();
    foreach ($files as $uuid => $filename) {
      $this->contentStorage->delete("file.$uuid");
      $this->unIndexContentByName("file.$uuid");
      $this->fileSync->deleteFile($filename);
    }
    \Drupal::messenger()->addMessage('All unused files have been deleted.');
  }

}
+7 −0
Original line number Diff line number Diff line
@@ -9,3 +9,10 @@ tome_sync.import_partial:
  description: 'Updates or deletes content and files based on what has changed in the export.'
  route_name: tome_sync.import_partial
  weight: 0

tome_sync.clean_files:
  title: 'Remove unused files'
  parent: tome_sync.main
  description: 'Finds files not referenced in config or content exports and deletes them.'
  route_name: tome_sync.clean_files
  weight: 1
+8 −0
Original line number Diff line number Diff line
@@ -13,3 +13,11 @@ tome_sync.import_partial:
    _title: 'Synchronize content and files'
  requirements:
    _permission: 'use tome sync'

tome_sync.clean_files:
  path: '/admin/config/tome/sync/clean-files'
  defaults:
    _form: 'Drupal\tome_sync\Form\CleanFilesForm'
    _title: 'Remove unused files'
  requirements:
    _permission: 'use tome sync'
Loading