file_checker-8.x-1.0-alpha2/src/BulkFileChecking.php

src/BulkFileChecking.php
<?php

namespace Drupal\file_checker;

use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Routing\RouteProvider;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;

const MAX_SET_SIZE = 5000;
const MIN_SET_SIZE = 25;

/**
 * Creates a BulkFileChecking object.
 */
class BulkFileChecking {
  use StringTranslationTrait;

  /**
   * The state service.
   *
   * @var \Drupal\Core\State\StateInterface $state
   */
  protected $state;

  /**
   * The File Checker logger channel.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface $logger
   */
  protected $logger;

  /**
   * The entity query service.
   *
   * @var QueryFactory $queryFactory
   */
  protected $queryFactory;

  /**
   * The date formatter service.
   *
   * @var \Drupal\Core\Datetime\DateFormatterInterface
   */
  protected $dateFormatter;

  /**
   * The route provider.
   *
   * @var \Drupal\Core\Routing\RouteProvider
   */
  protected $routeProvider;

  /**
   * The FileChecker SingleFileChecking service.
   */
  protected $singleFileChecking;

  /**
   * Constructs a FileCheckerManager object.
   *
   * @param \Drupal\Core\State\StateInterface $state
   *   The state service.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger channel factory service.
   * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
   *   The entity query factory service.
   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
   *   The date formatter service.
   * @param $single_file_checking
   *   The FileChecker SingleFileChecking service.
   * @param $route_provider
   *   The route provider service.
   */
  public function __construct(StateInterface $state, LoggerChannelFactoryInterface $logger_factory, QueryFactory $query_factory, DateFormatterInterface $date_formatter, $single_file_checking, RouteProvider $route_provider) {
    $this->state = $state;
    $this->logger = $logger_factory->get('file_checker');
    $this->queryFactory = $query_factory;
    $this->dateFormatter = $date_formatter;
    $this->routeProvider = $route_provider;
    $this->singleFileChecking = $single_file_checking;
  }

  /**
   * Start a new bulk file checking run.
   */
  public function start() {
    if ($this->hasBeenRequested()) {
      $this->logger->notice("Bulk file checking has already been requested.");
      return FALSE;
    }
    $this->reset();
    $this->state->set('file_checker.background_requested', TRUE);
    $this->logger->notice("Bulk file checking requested.");
    return TRUE;
  }

  /**
   * Execute bulk file checking, probably from cron or drush.
   *
   * @param int $timeLimit
   *   How many seconds to check files for.
   * @param bool $shouldLog
   *   Whether to log this execution.
   *
   * @return int
   *   The fid of the last file checked.
   */
  public function executeInBackground($timeLimit, $shouldLog = FALSE) {
    // Only execute checking if it has been requested.
    // For example, this function may be called every minute by cron,
    // to do a 1 minute chunk of work, but that doesn't mean we want to be
    // checking files continuously.
    if (!$this->hasBeenRequested()) {
      $runState['aborted'] = TRUE;
      return $runState;
    }
    $runState['aborted'] = FALSE;

    // If this is the first execution within a run, set the start time.
    if (empty($this->state->get('file_checker.background_run_start'))) {
      $this->state->set('file_checker.background_run_start', time());
    }

    // Load the state of the checking run.
    $previousLastCheckedFile = $this->state->get('file_checker.background_last_checked_file');
    $previousFilesMissingCount = $this->state->get('file_checker.background_files_missing_count');
    $previousFilesCheckedCount = $this->state->get('file_checker.background_files_checked_count');
    $runState['last_checked_file'] = $previousLastCheckedFile;
    $runState['files_missing_count'] = $previousFilesMissingCount;
    $runState['files_checked_count'] = $previousFilesCheckedCount;
    $runState['speed'] = $this->state->get('file_checker.speed');

    // Check files.
    $runState = $this->checkForSomeTime($timeLimit, $runState);
    $runState['files_just_checked'] = $runState['files_checked_count'] - $previousFilesCheckedCount;
    $runState['files_to_check'] = $this->filesCount();

    // Log this execution
    if ($shouldLog) {
      $this->logger->notice($runState['files_just_checked'] . " files just checked, " . $this->state->get('file_checker.speed') . " per second.");
    }

    // See if the checking run has stopped.
    if ($previousLastCheckedFile != $runState['last_checked_file']) {
      // If not stopped, store the state of the checking run ready to resume.
      $this->state->set('file_checker.background_last_checked_file', $runState['last_checked_file']);
      $this->state->set('file_checker.background_files_missing_count', $runState['files_missing_count']);
      $this->state->set('file_checker.background_files_checked_count', $runState['files_checked_count']);
      $this->state->set('file_checker.speed', $runState['speed']);
      $runState['finished'] = FALSE;
      return $runState;
    }
    else {
      // If it has stopped, conclude the checking run.
      $this->conclude($runState['files_missing_count'], $runState['files_checked_count'], $this->state->get('file_checker.background_run_start'));
      $this->reset();
      $runState['finished'] = TRUE;
      return $runState;
    }
  }

  /**
   * Batch check callback used when running by batch API from UI.
   */
  public function executeInUI($timeLimit, &$context) {
    // If this is a new batch API run, initialise the context.
    if (empty($context['sandbox'])) {
      $this->logger->notice("File checking initiated using batch API.");
      $context['sandbox']['files_to_check'] = $this->filesCount();
      $context['sandbox']['run_start'] = time();
      $context['sandbox']['run_state']['last_checked_file'] = 0;
      $context['sandbox']['run_state']['files_missing_count'] = 0;
      $context['sandbox']['run_state']['files_checked_count'] = 0;
      $context['sandbox']['run_state']['speed'] = $this->state->get('file_checker.speed');
    }
    $previousLastChecked = $context['sandbox']['run_state']['last_checked_file'];

    // Check files.
    $context['sandbox']['run_state'] = $this->checkForSomeTime($timeLimit, $context['sandbox']['run_state']);

    // See if the checking run has stopped.
    // We only stop when no newer files can be loaded, not when we think we've
    // done a certain number of files. This is because the number of file
    // entities could change over the course of the run.
    if ($previousLastChecked != $context['sandbox']['run_state']['last_checked_file']) {
      // If it has not stopped, inform batch API of proportion completed.
      // Must be less than 1 or we trigger batch API finishing without a final
      // check for new files.
      $context['finished'] = min(0.999, $context['sandbox']['run_state']['files_checked_count'] / $context['sandbox']['files_to_check']);
    }
    else {
      // If it has stopped, conclude the batch API processing.
      $context['finished'] = 1;
      $this->conclude($context['sandbox']['run_state']['files_missing_count'], $context['sandbox']['run_state']['files_checked_count'], $context['sandbox']['run_start']);
    }
  }

  /**
   * Bulk check files for a period of time.
   *
   * @param int $timeLimit
   *   How many seconds to check files for.
   *
   * @return int
   *   The fid of the last file checked.
   */
  public function checkForSomeTime($timeLimit, $runState) {
    $endTime = time() + $timeLimit;
    // This will usually loop only once, as a single call to checkFiles
    // inside this loop should take up all the allotted time.
    while (time() < $endTime) {
      $remainingTime = $endTime - time();
      // Sets of files to check should contain twice as many files as we expect
      // to need as it's more inefficient to have to load a second set than it
      // is to load an over-large batch. But sets should not be too large, or
      // they consume too much memory.
      $setSize = min(MAX_SET_SIZE, 2 * ($runState['speed'] * $remainingTime));
      // Tiny sets are also undesirable - they can occur if a previous
      // batch stalled, for example due to a remote server not responding.
      $setSize = max(MIN_SET_SIZE, $setSize);

      // Get the next files with fid greater than the last checked file.
      $fileIds = $this->query()
        ->condition('fid', $runState['last_checked_file'], '>')
        ->sort('fid', 'ASC')
        ->range(0, $setSize)
        ->execute();

      // If there are files subsequent to the last checked file, check them.
      if (count($fileIds) > 0) {
        $runState = $this->checkFiles($fileIds, $runState, $endTime);
      }
      // If there are not, then checking has been completed.
      else {
        break;
      }
    }
    return $runState;
  }

  /**
   * Bulk check a set of files.
   *
   * @param array $fileIds
   *   The file Ids to check.
   * @param int $runState
   *   An array that tracks progress of the current run.
   * @param int $endTime
   *   The time to stop checking regardless of progress.
   *
   * @return int
   *   The fid of the last file checked.
   */
  protected function checkFiles($fileIds, $runState, $endTime) {
    // Check the files until we reach the end of the batch or run out of time.
    $startTime = time();
    $startCheckedCount = $runState['files_checked_count'];
    foreach ($fileIds as $fileId) {
      if (time() > ($endTime)) {
        break;
      }
      $fileIsMissing = $this->singleFileChecking->checkFileFromId($fileId);
      if ($fileIsMissing) {
        $runState['files_missing_count']++;
      }
      $runState['files_checked_count']++;
      $runState['last_checked_file'] = $fileId;
    }
    $runState['speed'] = ($runState['files_checked_count'] - $startCheckedCount) / max(1, (time() - $startTime));
    return $runState;
  }

  /**
   * Specify the file entities to check.
   *
   * @return \Drupal\Core\Entity\Query\QueryInterface
   *   An entity query object selecting the file entities to be checked.
   */
  public function query() {
    return $this->queryFactory->get('file');
  }

  /**
   * How many file entities are eligible for checking.
   *
   * @return int
   *   A count of how many file entities are eligible for checking.
   */
  public function filesCount() {
    return $this->query()->count()->execute();
  }

  /**
   * Resets bulk File Checker's internal state variables.
   */
  protected function reset() {
    $this->state->set('file_checker.background_last_checked_file', 0);
    $this->state->set('file_checker.background_files_checked_count', 0);
    $this->state->set('file_checker.background_files_missing_count', 0);
    $this->state->set('file_checker.background_requested', FALSE);
    $this->state->set('file_checker.background_run_start', NULL);
  }

  /**
   * Log the results of the file checking run.
   */
  protected function conclude($filesMissingCount, $filesCheckedCount, $runStart) {
    $this->state->set('file_checker.last_run_start', $runStart);
    $this->state->set('file_checker.last_run_end', time());
    $missingReport = $this->formatPlural($filesMissingCount, '1 missing file detected,', '@count missing files detected,');
    $checkedReport = $this->formatPlural($filesCheckedCount, '1 file checked.', '@count files checked.');
    if ($filesMissingCount > 0) {
      $this->logger->warning($missingReport . ' ' . $checkedReport);
    }
    else {
      $this->logger->notice($missingReport . ' ' . $checkedReport);
    }
  }

  /**
   * Cancels initiated bulk file checking.
   */
  public function cancel() {
    $this->reset();
    $this->logger->notice("File checking cancelled by user.");
  }

  /**
   * Cancels initiated bulk file checking.
   */
  public function hasBeenRequested() {
    return ($this->state->get('file_checker.background_requested'));
  }

  /**
   * Compiles a report about the last completed file checking bulk run.
   *
   * @return string
   *    Text describing the last File Checker bulk run and its results.
   */
  public function lastStatus() {
    $last_run_start = $this->state->get('file_checker.last_run_start');
    if (!is_integer($last_run_start)) {
      $statusReport = t("Files have never been checked.");
    }
    else {
      $last_run_end = $this->state->get('file_checker.last_run_end');
      if (!is_integer($last_run_end)) {
        # This should never happen.
        $statusReport = t("A run started but its end has not been recorded.");
      }
      else {
        $ago = $this->dateFormatter->formatTimeDiffSince($last_run_end);
        $duration = $this->dateFormatter->formatDiff($last_run_start, $last_run_end);
        $statusReport = t("Last check completed @time_elapsed ago, took @duration.", [
          '@time_elapsed' => $ago,
          '@duration' => $duration
        ]);
      }
    }
    return $statusReport;
  }

  /**
   * Compiles a report about in progress background bulk file checking.
   *
   * @return string
   *   Text describing the progress of the current background file checking run.
   */
  public function backgroundStatus() {
    $statusReport = '';
    if (!empty($this->state->get('file_checker.background_requested'))) {
      $stateReport = t("Background file checking has been requested.");
      $filesReport = $this->formatPlural($this->filesCount(), "Currently 1 file to check.", "Currently @count files to check.");
      $runStart = $this->state->get('file_checker.background_run_start');
      if (!empty($runStart)) {
        $stateReport = t("Background file checking has started.");
        $runStartedAgo = $this->dateFormatter->formatTimeDiffSince($runStart);
        $startedReport = t("Checking started @time_elapsed ago:", ['@time_elapsed' => $runStartedAgo]);
        $checkedCount = $this->state->get('file_checker.background_files_checked_count');
        $checkedReport = $this->formatPlural($checkedCount, "1 file checked so far, ", "@count files checked so far, ");
        $missingCount = $this->state->get('file_checker.background_files_missing_count');
        $missingReport = $this->formatPlural($missingCount, "detected 1 missing.", "detected @count missing.");
        $progressReport = $startedReport . ' ' . $checkedReport . $missingReport;
      }
      else {
        $progressReport = t("Actual file checking has not yet started.");
      }
      $statusReport = $stateReport . ' ' . $filesReport . ' ' . $progressReport;
    }
    return $statusReport;
  }

  /**
   * Count how many files have been recorded as missing.
   *
   * @return int
   *   The number of missing files.
   */
  public function missingCount() {
    return $this->query()
      ->condition('missing', TRUE)
      ->count()
      ->execute();
  }

  /**
   * Create a report about the number of missing files.
   *
   * @return array
   *   A render array with link to view missing files.
   */
  public function missingStatus() {
    $missingCount = $this->missingCount();
    if ($missingCount == 0) {
      $missingReport = array(
        '#markup' => $missingReport = t("No missing files have been detected."),
      );
    }
    else {
      $missingReport = $this->formatPlural($missingCount, "1 file is missing.", "@count files are missing.");

      // The url cannot be generated if the view has been deleted by a sitebuilder.
      // It's tricky to trap exceptions from Url::fromRoute()
      $viewRoute = 'view.files_missing.page_1';
      $viewExists = count($this->routeProvider->getRoutesByNames([$viewRoute])) === 1;
      if ($viewExists) {
        $missingReport = [
          '#type' => 'link',
          '#title' => $missingReport,
          '#url' => Url::fromRoute($viewRoute),
        ];
      }
    }
    return $missingReport;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc