Your IP : 3.15.5.27


Current Path : /var/www/axolotl/data/www/yar.axolotls.ru/bitrix/modules/disk/lib/volume/
Upload File :
Current File : /var/www/axolotl/data/www/yar.axolotls.ru/bitrix/modules/disk/lib/volume/cleaner.php

<?php

namespace Bitrix\Disk\Volume;

use Bitrix\Main;
use Bitrix\Main\Entity;
use Bitrix\Disk\Volume;
use Bitrix\Disk\Internals\Error\Error;
use Bitrix\Disk\Internals\Error\ErrorCollection;
use Bitrix\Disk\Internals\Error\IErrorable;


/**
 * Disk cleanlier class.
 * @package Bitrix\Disk\Volume
 */
class Cleaner implements IErrorable, Volume\IVolumeTimeLimit
{
	/** @implements Volume\IVolumeTimeLimit */
	use Volume\TimeLimit;

	/** @var ErrorCollection */
	private $errorCollection;

	/** @var Volume\Task */
	private $task;

	/** @var int Owner id */
	private $ownerId;

	/** @var \Bitrix\Disk\User */
	private $owner;

	// interval agent start
	const AGENT_INTERVAL = 10;

	// fix every n interaction
	const STATUS_FIX_INTERVAL = 20;

	// limit maximum number selected files
	const MAX_FILE_PER_INTERACTION = 1000;

	// limit maximum number selected folders
	const MAX_FOLDER_PER_INTERACTION = 1000;


	/**
	 * @param int $ownerId Whom will mark as deleted by.
	 */
	public function __construct($ownerId = \Bitrix\Disk\SystemUser::SYSTEM_USER_ID)
	{
		$this->ownerId = $ownerId;
	}


	/**
	 * Gets task.
	 * @return Volume\Task
	 */
	public function instanceTask()
	{
		if (!($this->task instanceof Volume\Task))
		{
			$this->task = new Volume\Task();
		}

		return $this->task;
	}

	/**
	 * Loads task.
	 * @param int $filterId Id of saved indicator result from b_disk_volume.
	 * @param int $ownerId Whom will mark as deleted by.
	 * @return boolean
	 */
	public function loadTask($filterId, $ownerId = \Bitrix\Disk\SystemUser::SYSTEM_USER_ID)
	{
		$this->instanceTask();
		if ($filterId > 0)
		{
			if(!$this->instanceTask()->loadTaskById($filterId, ($ownerId > 0 ? $ownerId : $this->ownerId)))
			{
				$this->collectError(new Error('Cleaner task not found', 'CLEANER_TASK_NOT_FOUND'));
				return false;
			}
		}

		if ($this->instanceTask()->getOwnerId() > 0)
		{
			$this->ownerId = $this->instanceTask()->getOwnerId();
		}

		return ($this->instanceTask()->getId() > 0);
	}


	/**
	 * Returns the fully qualified name of this class.
	 * @return string
	 */
	public static function className()
	{
		return get_called_class();
	}


	/**
	 * Returns agent's name.
	 * @param int|string $filterId Id of saved indicator result from b_disk_volume.
	 * @return string
	 */
	public static function agentName($filterId)
	{
		return static::className()."::runWorker({$filterId});";
	}

	/**
	 * Determines if a script is loaded via cron/command line.
	 * @return bool
	 */
	public static function isCronRun()
	{
		$isCronRun = false;
		if (
			!\Bitrix\Main\ModuleManager::isModuleInstalled('bitrix24') &&
			(php_sapi_name() === 'cli')
		)
		{
			$isCronRun = true;
		}

		return $isCronRun;
	}


	/**
	 * Runs clean process.
	 * @param int $filterId Id of saved indicator result from b_disk_volume.
	 * @return string
	 */
	public static function runWorker($filterId)
	{
		// only one interaction per hit
		if (self::isCronRun() === false)
		{
			if (defined(__NAMESPACE__ . '\\CLEANER_RUN_WORKER_LOCK'))
			{
				// do nothing, repeat
				return static::agentName($filterId);
			}
		}

		$cleaner = new static();
		if ($cleaner->loadTask($filterId) === false)
		{
			return '';// task not found
		}

		if (Volume\Task::isRunningMode($cleaner->instanceTask()->getStatus()) === false)
		{
			return '';// non running state
		}

		$cleaner->startTimer();

		$indicator = $cleaner->instanceTask()->getIndicator();
		if (!$indicator instanceof Volume\IVolumeIndicator)
		{
			return '';
		}

		if (!defined(__NAMESPACE__ . '\\CLEANER_RUN_WORKER_LOCK'))
		{
			define(__NAMESPACE__ . '\\CLEANER_RUN_WORKER_LOCK', true);
		}

		if ($cleaner->instanceTask()->getStatus() != Volume\Task::TASK_STATUS_RUNNING)
		{
			$cleaner->instanceTask()->setStatus(Volume\Task::TASK_STATUS_RUNNING);
		}

		// subTask to run
		$subTask = '';
		if (Volume\Task::isRunningMode($cleaner->instanceTask()->getStatusSubTask(Volume\Task::DROP_TRASHCAN)))
		{
			$subTask = Volume\Task::DROP_TRASHCAN;
		}
		elseif (Volume\Task::isRunningMode($cleaner->instanceTask()->getStatusSubTask(Volume\Task::EMPTY_FOLDER)))
		{
			$subTask = Volume\Task::EMPTY_FOLDER;
		}
		elseif (Volume\Task::isRunningMode($cleaner->instanceTask()->getStatusSubTask(Volume\Task::DROP_FOLDER)))
		{
			$subTask = Volume\Task::DROP_FOLDER;
		}
		elseif (Volume\Task::isRunningMode($cleaner->instanceTask()->getStatusSubTask(Volume\Task::DROP_UNNECESSARY_VERSION)))
		{
			$subTask = Volume\Task::DROP_UNNECESSARY_VERSION;
		}

		$repeatMeasure = function () use ($cleaner, $indicator)
		{
			// reset offset
			$cleaner->instanceTask()->setLastFileId(0);
			$cleaner->instanceTask()->fixState();

			$retry = 1;
			while ($retry <= 2)
			{
				try
				{
					// check final result repeat measure
					\Bitrix\Disk\Volume\Cleaner::repeatMeasure($indicator);
				}
				catch (Main\DB\SqlQueryException $exception)
				{
					if (stripos($exception->getMessage(), 'deadlock found when trying to get lock; try restarting transaction') !== false)
					{
						// retrying in a few microseconds
						usleep(100);
						$retry ++;
						continue;
					}

					throw $exception;
				}
				break;
			}

			// reload task
			$cleaner->instanceTask()->loadTaskById($indicator->getFilterId(), $cleaner->instanceTask()->getOwnerId());
		};

		// run subTask
		$taskDone = false;
		switch ($subTask)
		{
			case Volume\Task::DROP_TRASHCAN:
			{
				if ($cleaner->instanceTask()->getStatusSubTask($subTask) != Volume\Task::TASK_STATUS_RUNNING)
				{
					$cleaner->instanceTask()->setStatusSubTask($subTask, Volume\Task::TASK_STATUS_RUNNING);
				}

				if($cleaner->deleteTrashcanByFilter($indicator))
				{
					$repeatMeasure();
					$taskDone = $cleaner->instanceTask()->hasTaskFinished($subTask);
				}
				elseif ($cleaner->instanceTask()->hasFatalError())
				{
					$taskDone = true;
				}

				break;
			}

			case Volume\Task::EMPTY_FOLDER:
			case Volume\Task::DROP_FOLDER:
			{
				if ($cleaner->instanceTask()->getStatusSubTask($subTask) != Volume\Task::TASK_STATUS_RUNNING)
				{
					$cleaner->instanceTask()->setStatusSubTask($subTask, Volume\Task::TASK_STATUS_RUNNING);
				}

				$folderId = $cleaner->instanceTask()->getParam('FOLDER_ID');
				$folder = \Bitrix\Disk\Folder::getById($folderId);
				if ($folder instanceof \Bitrix\Disk\Folder)
				{
					if ($cleaner->deleteFolder($folder, ($subTask === Volume\Task::EMPTY_FOLDER)))
					{
						$repeatMeasure();
						$taskDone = $cleaner->instanceTask()->hasTaskFinished($subTask);
					}
					elseif ($cleaner->instanceTask()->hasFatalError())
					{
						$taskDone = true;
					}
				}
				else
				{
					$cleaner->instanceTask()->setLastError('Can not found folder #'.$folderId);
					$cleaner->instanceTask()->raiseFatalError();
					$taskDone = true;
				}

				break;
			}

			case Volume\Task::DROP_UNNECESSARY_VERSION:
			{
				if ($cleaner->instanceTask()->getStatusSubTask($subTask) != Volume\Task::TASK_STATUS_RUNNING)
				{
					$cleaner->instanceTask()->setStatusSubTask($subTask, Volume\Task::TASK_STATUS_RUNNING);
				}

				if($cleaner->deleteUnnecessaryVersionByFilter($indicator))
				{
					$repeatMeasure();
					$taskDone = $cleaner->instanceTask()->hasTaskFinished($subTask);
				}
				elseif ($cleaner->instanceTask()->hasFatalError())
				{
					$taskDone = true;
				}

				break;
			}

			default:
			{
				$taskDone = true;
			}
		}

		if($taskDone)
		{
			// finish
			$cleaner->instanceTask()->setStatusSubTask($subTask, Volume\Task::TASK_STATUS_DONE);
			$cleaner->instanceTask()->setStatus(Volume\Task::TASK_STATUS_DONE);
		}

		// Fix task state
		$cleaner->instanceTask()->fixState();

		// count statistic for progress bar
		self::countWorker($cleaner->instanceTask()->getOwnerId());

		if($taskDone)
		{
			return '';
		}

		return static::agentName($filterId);
	}


	/**
	 * Adds delayed delete worker agent.
	 * @param array $params Named parameters:
	 * 		int ownerId - who is owner,
	 * 		int filterId - as row private id from b_disk_volume as filter id,
	 * 		int storageId - limit only one storage
	 * 		int delay - number seconds to delay first execution
	 * 		bool DROP_UNNECESSARY_VERSION - set job to delete unused version,
	 * 		bool DROP_TRASHCAN - set job to empty trashcan.
	 * 		bool DROP_FOLDER - set job to drop everything.
	 * 		bool EMPTY_FOLDER - set job to empty folder structure.
	 * @return boolean
	 */
	public static function addWorker($params)
	{
		$ownerId = (int)$params['ownerId'];
		$filterId = (int)$params['filterId'];

		$task = new Volume\Task();
		if ($filterId > 0)
		{
			if(!$task->loadTaskById($filterId, $ownerId))
			{
				return false;
			}
			$ownerId = $task->getOwnerId();
		}

		$task->setStatus(Volume\Task::TASK_STATUS_WAIT);

		$subTaskCommands = array(
			Volume\Task::DROP_UNNECESSARY_VERSION,
			Volume\Task::DROP_TRASHCAN,
			Volume\Task::DROP_FOLDER,
			Volume\Task::EMPTY_FOLDER,
		);
		foreach ($subTaskCommands as $command)
		{
			if (isset($params[$command]))
			{
				$task->setStatusSubTask(
					$command,
					(($params[$command] === true) ? Volume\Task::TASK_STATUS_WAIT : Volume\Task::TASK_STATUS_NONE)
				);
			}
		}

		if ($filterId > 0)
		{
			if (isset($params['manual']))
			{
				$task->resetFail();
			}
			$agentParamsAdded = $task->fixState();
		}
		else
		{
			$task->setIndicatorType(Volume\Storage\Storage::className());
			$task->setParam('STORAGE_ID', (int)$params['storageId']);
			$task->setOwnerId($ownerId);

			$agentParamsAdded = $task->fixState();
			$filterId = $task->getId();
		}

		$nextExecutionTime = '';
		if (!empty($params['delay']) && (int)$params['delay'] > 0)
		{
			$nextExecutionTime = \ConvertTimeStamp(time()+\CTimeZone::GetOffset() + (int)$params['delay'], "FULL");
		}

		$agentAdded = false;
		if ($agentParamsAdded && $filterId > 0)
		{
			$agentAdded = true;
			$agents = \CAgent::GetList(
				array('ID' => 'DESC'),
				array('=NAME' => static::agentName($filterId))
			);
			if (!$agents->Fetch())
			{
				$agentAdded = (bool)(\CAgent::AddAgent(
										static::agentName($filterId),
										'disk',
										(self::canAgentUseCrontab() ? 'N' : 'Y'),
										self::AGENT_INTERVAL,
										'',
										'Y',
										$nextExecutionTime
									) !== false);
			}
		}

		// count statistic for progress bar
		self::countWorker($ownerId);

		return $agentAdded;
	}


	/**
	 * Checks ability agent to use Crontab.
	 * @return bool
	 */
	public static function canAgentUseCrontab()
	{
		$canAgentsUseCrontab = false;
		$agentsUseCrontab = \Bitrix\Main\Config\Option::get('main', 'agents_use_crontab', 'N');
		if (
			!\Bitrix\Main\ModuleManager::isModuleInstalled('bitrix24') &&
			($agentsUseCrontab === 'Y' || (defined('BX_CRONTAB_SUPPORT') && BX_CRONTAB_SUPPORT === true))
		)
		{
			$canAgentsUseCrontab = true;
		}

		return $canAgentsUseCrontab;
	}

	/**
	 * Cancels all agent process.
	 * @param int $ownerId Whom will mark as deleted by.
	 * @return void
	 */
	public static function cancelWorkers($ownerId)
	{
		$workerResult = \Bitrix\Disk\Internals\VolumeTable::getList(array(
			'select' => array(
				'ID',
			),
			'filter' => array(
				'=OWNER_ID' => $ownerId,
				'=AGENT_LOCK' => array(Volume\Task::TASK_STATUS_WAIT, Volume\Task::TASK_STATUS_RUNNING),
			)
		));
		foreach ($workerResult as $row)
		{
			\Bitrix\Disk\Internals\VolumeTable::update($row['ID'], array('AGENT_LOCK' => Volume\Task::TASK_STATUS_CANCEL));
		}

		self::clearProgressInfo($ownerId);
	}


	/**
	 * Count worker agent for user.
	 * @param int $ownerId Whom will mark as deleted by.
	 * @return int
	 */
	public static function countWorker($ownerId)
	{
		$workerResult = \Bitrix\Disk\Internals\VolumeTable::getList(array(
			'runtime' => array(
				new Entity\ExpressionField('CNT', 'COUNT(*)'),
				new Entity\ExpressionField('FILE_COUNT', 'SUM(FILE_COUNT)'),
				new Entity\ExpressionField('UNNECESSARY_VERSION_COUNT', 'SUM(UNNECESSARY_VERSION_COUNT)'),
				new Entity\ExpressionField('DROPPED_FILE_COUNT', 'SUM(DROPPED_FILE_COUNT)'),
				new Entity\ExpressionField('DROPPED_VERSION_COUNT', 'SUM(DROPPED_VERSION_COUNT)'),
				new Entity\ExpressionField('FAIL_COUNT', 'SUM(FAIL_COUNT)'),
			),
			'select' => array(
				'CNT',
				'FILE_COUNT',
				'UNNECESSARY_VERSION_COUNT',
				'DROPPED_FILE_COUNT',
				'DROPPED_VERSION_COUNT',
				'FAIL_COUNT',
				\Bitrix\Disk\Volume\Task::DROP_UNNECESSARY_VERSION,
				\Bitrix\Disk\Volume\Task::DROP_TRASHCAN,
				\Bitrix\Disk\Volume\Task::EMPTY_FOLDER,
				\Bitrix\Disk\Volume\Task::DROP_FOLDER,
			),
			'group' => array(
				\Bitrix\Disk\Volume\Task::DROP_UNNECESSARY_VERSION,
				\Bitrix\Disk\Volume\Task::DROP_TRASHCAN,
				\Bitrix\Disk\Volume\Task::EMPTY_FOLDER,
				\Bitrix\Disk\Volume\Task::DROP_FOLDER,
			),
			'filter' => array(
				'=OWNER_ID' => $ownerId,
				'=AGENT_LOCK' => array(Volume\Task::TASK_STATUS_WAIT, Volume\Task::TASK_STATUS_RUNNING),
			)
		));

		$totalFilesToDrop = 0;
		$droppedFilesCount = 0;
		$workerCount = 0;
		$failCount = 0;

		if ($workerResult->getSelectedRowsCount() > 0)
		{
			foreach ($workerResult as $row)
			{
				$workerCount += $row['CNT'];
				$failCount += $row['FAIL_COUNT'];
				if (Volume\Task::isRunningMode($row[\Bitrix\Disk\Volume\Task::DROP_UNNECESSARY_VERSION]))
				{
					$totalFilesToDrop += $row['UNNECESSARY_VERSION_COUNT'];
					$droppedFilesCount += $row['DROPPED_VERSION_COUNT'];
				}
				if (Volume\Task::isRunningMode($row[\Bitrix\Disk\Volume\Task::DROP_TRASHCAN]))
				{
					$totalFilesToDrop += $row['FILE_COUNT'];
					$droppedFilesCount += $row['DROPPED_FILE_COUNT'];
				}
				if (Volume\Task::isRunningMode($row[\Bitrix\Disk\Volume\Task::DROP_FOLDER]))
				{
					$totalFilesToDrop += $row['FILE_COUNT'];
					$droppedFilesCount += $row['DROPPED_FILE_COUNT'];
				}
				if (Volume\Task::isRunningMode($row[\Bitrix\Disk\Volume\Task::EMPTY_FOLDER]))
				{
					$totalFilesToDrop += $row['FILE_COUNT'];
					$droppedFilesCount += $row['DROPPED_FILE_COUNT'];
				}
			}
			self::setProgressInfo($ownerId, $totalFilesToDrop, $droppedFilesCount, $failCount);
		}
		else
		{
			self::clearProgressInfo($ownerId);
		}

		return $workerCount;
	}


	/**
	 * Check if workers exists. Sets up/removes missing task. Remove stepper info.
	 * @param int $ownerId Whom will mark as deleted by.
	 * @return int
	 */
	public static function checkRestoreWorkers($ownerId)
	{
		$workerResult = \Bitrix\Disk\Internals\VolumeTable::getList(array(
			'select' => array(
				'ID',
				'STORAGE_ID',
				\Bitrix\Disk\Volume\Task::DROP_UNNECESSARY_VERSION,
				\Bitrix\Disk\Volume\Task::DROP_TRASHCAN,
				\Bitrix\Disk\Volume\Task::EMPTY_FOLDER,
				\Bitrix\Disk\Volume\Task::DROP_FOLDER,
			),
			'filter' => array(
				'=OWNER_ID' => $ownerId,
				'=AGENT_LOCK' => array(Volume\Task::TASK_STATUS_WAIT, Volume\Task::TASK_STATUS_RUNNING),
			)
		));
		$workerCount = 0;
		if ($workerResult->getSelectedRowsCount() > 0)
		{
			$agents = \CAgent::GetList(
				array('ID' => 'DESC'),
				array('NAME' => self::agentName('%'))
			);
			$agentList = array();
			while ($agent = $agents->Fetch())
			{
				$agentList[] = $agent['NAME'];
			}

			$restoredWorkerCount = 0;
			foreach ($workerResult as $row)
			{
				$workerCount ++;
				if (in_array(self::agentName($row['ID']), $agentList) === false)
				{
					$agentParams = array(
						'ownerId' => $ownerId,
						'storageId' => $row['STORAGE_ID'],
						'filterId' => $row['ID'],
					);
					if (Volume\Task::isRunningMode($row[\Bitrix\Disk\Volume\Task::DROP_UNNECESSARY_VERSION]))
					{
						$agentParams[\Bitrix\Disk\Volume\Task::DROP_UNNECESSARY_VERSION] = true;
					}
					if (Volume\Task::isRunningMode($row[\Bitrix\Disk\Volume\Task::DROP_TRASHCAN]))
					{
						$agentParams[\Bitrix\Disk\Volume\Task::DROP_TRASHCAN] = true;
					}
					if (Volume\Task::isRunningMode($row[\Bitrix\Disk\Volume\Task::DROP_FOLDER]))
					{
						$agentParams[\Bitrix\Disk\Volume\Task::DROP_FOLDER] = true;
					}
					if (Volume\Task::isRunningMode($row[\Bitrix\Disk\Volume\Task::EMPTY_FOLDER]))
					{
						$agentParams[\Bitrix\Disk\Volume\Task::EMPTY_FOLDER] = true;
					}

					if (self::addWorker($agentParams))
					{
						$restoredWorkerCount ++;
					}
				}
			}

			if ($restoredWorkerCount > 0)
			{
				self::countWorker($ownerId);
			}
		}
		else
		{
			self::clearProgressInfo($ownerId);
		}

		return $workerCount;
	}


	/**
	 * Deletes files corresponding to indicator filter.
	 * @param Volume\IVolumeIndicator $indicator Ignited indicator for file list filter.
	 * @return boolean
	 */
	public function deleteFileByFilter(Volume\IVolumeIndicator $indicator)
	{
		$subTaskDone = true;

		if ($indicator->getFilterValue('STORAGE_ID') > 0)
		{
			$storage = \Bitrix\Disk\Storage::loadById($indicator->getFilterValue('STORAGE_ID'));
			if (!($storage instanceof \Bitrix\Disk\Storage))
			{
				$this->collectError(
					new Error('Can not found storage #'.$indicator->getFilterValue('STORAGE_ID'), 'STORAGE_NOT_FOUND'),
					false,
					true
				);

				return false;
			}
			if (!$this->isAllowClearStorage($storage))
			{
				$this->collectError(
					new Error('Access denied to storage #'.$storage->getId(), 'ACCESS_DENIED'),
					false,
					true
				);

				return false;
			}
		}

		$filter = array();
		if ($this->instanceTask()->getLastFileId() > 0)
		{
			$filter['>=ID'] = $this->instanceTask()->getLastFileId();
		}

		$indicator->setLimit(self::MAX_FILE_PER_INTERACTION);

		$fileList = $indicator->getCorrespondingFileList($filter);

		$this->instanceTask()->setIterationFileCount($fileList->getSelectedRowsCount());

		$countFileErasure = 0;

		foreach ($fileList as $row)
		{
			$fileId = $row['ID'];
			$file = \Bitrix\Disk\File::getById($fileId);
			if ($file instanceof \Bitrix\Disk\File)
			{
				if (!$this->isAllowClearFolder($file->getParent()))
				{
					$this->collectError(new Error("Access denied to file #$fileId", 'ACCESS_DENIED'));
				}
				else
				{
					$securityContext = $this->getSecurityContext($this->getOwner(), $file);
					if (!$file->canDelete($securityContext))
					{
						$this->collectError(new Error("Access denied to file #$fileId", 'ACCESS_DENIED'));
					}
					else
					{
						$this->deleteFile($file);
						$countFileErasure++;
					}
				}
			}

			$this->instanceTask()->setLastFileId($fileId);

			// fix interval task state
			if ($countFileErasure >= self::STATUS_FIX_INTERVAL)
			{
				$countFileErasure = 0;

				if ($this->instanceTask()->hasUserCanceled())
				{
					$subTaskDone = false;
					break;
				}

				$this->instanceTask()->fixState();

				// count statistic for progress bar
				self::countWorker($this->instanceTask()->getOwnerId());
			}

			if (!$this->checkTimeEnd())
			{
				$subTaskDone = false;
				break;
			}
		}

		return $subTaskDone;
	}


	/**
	 * Deletes files in trashcan.
	 * @param Volume\IVolumeIndicator $indicator Ignited indicator for file list filter.
	 * @return boolean
	 */
	public function deleteTrashcanByFilter(Volume\IVolumeIndicator $indicator)
	{
		$subTaskDone = true;

		$filter = array(
			'!DELETED_TYPE' => \Bitrix\Disk\Internals\ObjectTable::DELETED_TYPE_NONE
		);
		if ($this->instanceTask()->getLastFileId() > 0)
		{
			$filter['>=ID'] = $this->instanceTask()->getLastFileId();
		}

		$indicator->setLimit(self::MAX_FILE_PER_INTERACTION);

		$fileList = $indicator->getCorrespondingFileList($filter);

		$this->instanceTask()->setIterationFileCount($fileList->getSelectedRowsCount());

		$countFileErasure = 0;

		foreach ($fileList as $row)
		{
			$fileId = $row['ID'];
			$file = \Bitrix\Disk\File::getById($fileId);
			if ($file instanceof \Bitrix\Disk\File)
			{
				$securityContext = $this->getSecurityContext($this->getOwner(), $file);
				if($file->canDelete($securityContext))
				{
					$this->deleteFile($file);
					$countFileErasure ++;
				}
				else
				{
					$this->collectError(new Error("Access denied to file #$fileId", 'ACCESS_DENIED'));
				}
			}

			$this->instanceTask()->setLastFileId($fileId);

			// fix interval task state
			if ($countFileErasure >= self::STATUS_FIX_INTERVAL)
			{
				$countFileErasure = 0;

				if ($this->instanceTask()->hasUserCanceled())
				{
					$subTaskDone = false;
					break;
				}

				$this->instanceTask()->fixState();

				// count statistic for progress bar
				self::countWorker($this->instanceTask()->getOwnerId());
			}

			if (!$this->checkTimeEnd())
			{
				$subTaskDone = false;
				break;
			}
		}

		$indicator->setLimit(self::MAX_FOLDER_PER_INTERACTION);

		$folderList = $indicator->getCorrespondingFolderList(array('!DELETED_TYPE' => \Bitrix\Disk\Internals\ObjectTable::DELETED_TYPE_NONE));

		foreach ($folderList as $row)
		{
			$folder = \Bitrix\Disk\Folder::getById($row['ID']);
			if ($folder instanceof \Bitrix\Disk\Folder)
			{
				$this->deleteFolder($folder);
				$countFileErasure ++;
			}

			// fix interval task state
			if ($countFileErasure >= self::STATUS_FIX_INTERVAL)
			{
				$countFileErasure = 0;

				if ($this->instanceTask()->hasUserCanceled())
				{
					$subTaskDone = false;
					break;
				}

				$this->instanceTask()->fixState();

				// count statistic for progress bar
				self::countWorker($this->instanceTask()->getOwnerId());
			}

			if (!$this->checkTimeEnd())
			{
				$subTaskDone = false;
				break;
			}
		}

		return $subTaskDone;
	}


	/**
	 * Deletes unused file versions.
	 * @param Volume\IVolumeIndicator $indicator Ignited indicator for file list filter.
	 * @return boolean
	 */
	public function deleteUnnecessaryVersionByFilter(Volume\IVolumeIndicator $indicator)
	{
		$subTaskDone = true;

		if ($indicator->getFilterValue('STORAGE_ID') > 0)
		{
			$storage = \Bitrix\Disk\Storage::loadById($indicator->getFilterValue('STORAGE_ID'));
			if (!($storage instanceof \Bitrix\Disk\Storage))
			{
				$this->collectError(
					new Error('Can not found storage #'.$indicator->getFilterValue('STORAGE_ID'), 'STORAGE_NOT_FOUND'),
					false,
					true
				);

				return false;
			}
			if (!$this->isAllowClearStorage($storage))
			{
				$this->collectError(
					new Error('Access denied to storage #'.$storage->getId(), 'ACCESS_DENIED'),
					false,
					true
				);

				return false;
			}
		}

		$filter = array();
		if ($this->instanceTask()->getLastFileId() > 0)
		{
			$filter['>=FILE_ID'] = $this->instanceTask()->getLastFileId();
		}

		$indicator->setLimit(self::MAX_FILE_PER_INTERACTION);

		$versionList = $indicator->getCorrespondingUnnecessaryVersionList($filter);

		$this->instanceTask()->setIterationFileCount($versionList->getSelectedRowsCount());

		$versionsPerFile = array();
		foreach ($versionList as $row)
		{
			$fileId = $row['FILE_ID'];
			$versionId = $row['VERSION_ID'];
			if (!isset($versionsPerFile[$fileId]))
			{
				$versionsPerFile[$fileId] = array();
			}
			$versionsPerFile[$fileId][] = $versionId;
		}
		unset($row, $fileId, $versionId, $versionList);


		$countFileErasure = 0;

		foreach ($versionsPerFile as $fileId => $versionIds)
		{
			$file = \Bitrix\Disk\File::getById($fileId);

			if ($file instanceof \Bitrix\Disk\File)
			{
				$securityContext = $this->getSecurityContext($this->getOwner(), $file);
				if($file->canDelete($securityContext))
				{
					$this->deleteFileUnnecessaryVersion($file, array('=ID' => $versionIds));
					$countFileErasure++;
				}
				else
				{
					$this->collectError(new Error("Access denied to file #$fileId", 'ACCESS_DENIED'));
				}
			}

			$this->instanceTask()->setLastFileId($fileId);

			// fix interval task state
			if ($countFileErasure >= self::STATUS_FIX_INTERVAL)
			{
				$countFileErasure = 0;

				if ($this->instanceTask()->hasUserCanceled())
				{
					$subTaskDone = false;
					break;
				}

				$this->instanceTask()->fixState();

				// count statistic for progress bar
				self::countWorker($this->instanceTask()->getOwnerId());
			}

			if (!$this->checkTimeEnd())
			{
				$subTaskDone = false;
				break;
			}
		}

		return $subTaskDone;
	}


	/**
	 * Returns disk security context.
	 * @param \Bitrix\Disk\User $user Task owner.
	 * @param \Bitrix\Disk\BaseObject $object File or folder.
	 * @return \Bitrix\Disk\Security\SecurityContext
	 */
	private function getSecurityContext($user, $object)
	{
		static $securityContextCache = array();

		$userId = $user->getId();
		$storageId = $object->getStorageId();

		if (!($securityContextCache[$userId][$storageId] instanceof \Bitrix\Disk\Security\SecurityContext))
		{
			if (!isset($securityContextCache[$userId]))
			{
				$securityContextCache[$userId] = array();
			}

			if ($user->isAdmin())
			{
				$securityContextCache[$userId][$storageId] = new \Bitrix\Disk\Security\FakeSecurityContext($userId);
			}
			else
			{
				$securityContextCache[$userId][$storageId] = $object->getStorage()->getSecurityContext($userId);
			}
		}

		return $securityContextCache[$userId][$storageId];
	}


	/**
	 * Deletes file.
	 * @param \Bitrix\Disk\File $file File to drop.
	 * @return boolean
	 */
	public function deleteFile(\Bitrix\Disk\File $file)
	{
		try
		{
			$logData = $this->instanceTask()->collectLogData($file);

			if (!$file->delete($this->instanceTask()->getOwnerId()))
			{
				$this->collectError($file->getErrors());

				return false;
			}

			$this->instanceTask()->log($logData, __FUNCTION__);

			$this->instanceTask()->increaseDroppedFileCount();

		}
		catch (Main\SystemException $exception)
		{
			$this->collectError(new Error($exception->getMessage(), $exception->getCode()), true, false);

			return false;
		}

		return true;
	}


	/**
	 * Deletes file unnecessary versions.
	 * @param \Bitrix\Disk\File $file File to purify.
	 * @param array $additionalFilter Additional filter for vertion selection.
	 * @return boolean
	 */
	public function deleteFileUnnecessaryVersion(\Bitrix\Disk\File $file, $additionalFilter = array())
	{
		$subTaskDone = true;

		$filter = array(
			'=OBJECT_ID' => $file->getId(),
		);
		if (count($additionalFilter) > 0)
		{
			$filter = array_merge($filter, $additionalFilter);
		}

		$versionList = \Bitrix\Disk\Version::getList(array(
			'filter' => $filter,
			'select' => array('ID')
		));
		foreach ($versionList as $row)
		{
			$versionId = $row['ID'];

			/** @var \Bitrix\Disk\Version $version */
			$version = \Bitrix\Disk\Version::getById($versionId);
			if(!$version instanceof \Bitrix\Disk\Version)
			{
				//$this->collectError(new Error('Version '.$versionId.' was not found'));
				continue;
			}

			// is a head
			if ($version->getFileId() == $file->getFileId())
			{
				//$this->collectError(new Error('Version '.$versionId.' is a head'));
				continue;
			}

			// attached_object
			$attachedList = \Bitrix\Disk\AttachedObject::getList(array(
				'filter' => array(
					'=OBJECT_ID' => $file->getId(),
					'=VERSION_ID' => $version->getId(),
				),
				'select' => array('ID'),
				'limit' => 1,
			));
			if($attachedList->getSelectedRowsCount() > 0)
			{
				$this->collectError(new Error('Version '.$versionId.' has attachments'));
				continue;
			}

			// external_link
			$externalLinkList = \Bitrix\Disk\ExternalLink::getList(array(
				'filter' => array(
					'=OBJECT_ID' => $file->getId(),
					'=VERSION_ID' => $version->getId(),
					'!TYPE' => \Bitrix\Disk\ExternalLink::TYPE_AUTO,
				),
				'select' => array('ID'),
				'limit' => 1,
			));
			if($externalLinkList->getSelectedRowsCount() > 0)
			{
				$this->collectError(new Error('Version '.$versionId.' has external links'));
				continue;
			}

			$logData = $this->instanceTask()->collectLogData($version);

			try
			{
				// drop
				if (!$version->delete($this->instanceTask()->getOwnerId()))
				{
					$this->collectError($version->getErrors());
				}
				else
				{
					$this->instanceTask()->log($logData, __FUNCTION__);
					$this->instanceTask()->increaseDroppedVersionCount();
				}
			}
			catch (Main\SystemException $exception)
			{
				$this->collectError(new Error($exception->getMessage(), $exception->getCode()));
			}

			if (!$this->checkTimeEnd())
			{
				$subTaskDone = false;
				break;
			}
		}

		return $subTaskDone;
	}


	/**
	 * Deletes folder.
	 * @param \Bitrix\Disk\Folder $folder Folder to drop.
	 * @param boolean $emptyOnly Just delete folder's content.
	 * @return boolean
	 */
	public function deleteFolder(\Bitrix\Disk\Folder $folder, $emptyOnly = false)
	{
		$subTaskDone = true;

		if (!$this->isAllowClearStorage($folder->getStorage()))
		{
			$this->collectError(
				new Error('Access denied to storage #'. $folder->getStorageId(), 'ACCESS_DENIED'),
				false,
				true
			);

			return false;
		}

		if (!$this->isAllowClearFolder($folder))
		{
			$this->collectError(
				new Error('Not allowed to drop #'. $folder->getId(), 'ACCESS_DENIED'),
				false,
				true
			);

			return false;
		}

		// restrict delete root folder
		if ($folder->getStorage()->getRootObjectId() == $folder->getId())
		{
			$emptyOnly = true;
		}

		if (!$emptyOnly && !$this->isAllowDeleteFolder($folder))
		{
			$this->collectError(
				new Error('Not allowed to drop #'. $folder->getId(), 'ACCESS_DENIED'),
				false,
				false
			);

			return false;
		}

		$countFileErasure = 0;

		$objectList = \Bitrix\Disk\Internals\ObjectTable::getList(array(
			'filter' => array(
				'=PATH_CHILD.PARENT_ID' => $folder->getId(),
			),
			'order' => array(
				'PATH_CHILD.DEPTH_LEVEL' => 'DESC',
				'ID' => 'ASC'
			),
			'limit' => self::MAX_FOLDER_PER_INTERACTION,
		));

		$this->instanceTask()->setIterationFileCount($objectList->getSelectedRowsCount());

		foreach ($objectList as $row)
		{
			if ($row['ID'] == $folder->getId())
			{
				continue;
			}

			$object = \Bitrix\Disk\BaseObject::buildFromArray($row);

			/** @var Folder|File $object */
			if($object instanceof \Bitrix\Disk\Folder)
			{
				/** @var \Bitrix\Disk\File $object */
				$securityContext = $this->getSecurityContext($this->getOwner(), $object);
				if ($object->canDelete($securityContext))
				{
					if ($this->isAllowDeleteFolder($object))
					{
						try
						{
							$logData = $this->instanceTask()->collectLogData($object);

							/** @var \Bitrix\Disk\Folder $object */
							if (!$object->deleteTree($this->instanceTask()->getOwnerId()))
							{
								$this->collectError($object->getErrors(), false);

								$subTaskDone = false;
							}
							else
							{
								$this->instanceTask()->log($logData, __FUNCTION__);
								$this->instanceTask()->increaseDroppedFolderCount();
							}
						}
						catch (Main\SystemException $exception)
						{
							$this->collectError(
								new Error($exception->getMessage(), $exception->getCode()),
								true,
								false
							);
						}
					}
					else
					{
						$this->collectError(
							new Error('Not allowed to drop folder #'. $object->getId(), 'ACCESS_DENIED'),
							false,
							false
						);
					}
				}
				else
				{
					$this->collectError(
						new Error('Access denied to folder #'. $object->getId(), 'ACCESS_DENIED'),
						true,
						false
					);
				}
			}
			elseif($object instanceof \Bitrix\Disk\File)
			{
				/** @var \Bitrix\Disk\File $object */
				$securityContext = $this->getSecurityContext($this->getOwner(), $object);
				if($object->canDelete($securityContext))
				{
					$subTaskDone = $this->deleteFile($object);
				}
				else
				{
					$this->collectError(new Error('Access denied to file #'. $object->getId(), 'ACCESS_DENIED'));
				}
			}

			// fix interval task state
			$countFileErasure ++;
			if ($countFileErasure >= self::STATUS_FIX_INTERVAL)
			{
				$countFileErasure = 0;

				if ($this->instanceTask()->hasUserCanceled())
				{
					$subTaskDone = false;
					break;
				}

				$this->instanceTask()->fixState();

				// count statistic for progress bar
				self::countWorker($this->instanceTask()->getOwnerId());

			}

			if (!$this->checkTimeEnd())
			{
				$subTaskDone = false;
				break;
			}

		}

		if ($subTaskDone)
		{
			if ($emptyOnly === false)
			{
				try
				{
					$logData = $this->instanceTask()->collectLogData($folder);

					if (!$folder->deleteTree($this->instanceTask()->getOwnerId()))
					{
						$this->collectError($folder->getErrors());

						return false;
					}

					$this->instanceTask()->log($logData, __FUNCTION__);
					$this->instanceTask()->increaseDroppedFolderCount();
				}
				catch (Main\SystemException $exception)
				{
					$this->collectError(new Error($exception->getMessage(), $exception->getCode()));
				}
			}
		}

		return $subTaskDone;
	}


	/**
	 * Check ability to drop folder.
	 * @param \Bitrix\Disk\Folder $folder Folder to drop.
	 * @return boolean
	 */
	public function isAllowDeleteFolder(\Bitrix\Disk\Folder $folder)
	{
		$allowDrop = true;

		if ($folder->isDeleted())
		{
			return true;
		}

		/** @var \Bitrix\Disk\Volume\IDeleteConstraint[] $deleteConstraintList */
		static $deleteConstraintList;
		if (empty($deleteConstraintList))
		{
			$deleteConstraintList = array();

			// full list available indicators
			$constraintIdList = \Bitrix\Disk\Volume\Base::listDeleteConstraint();
			foreach ($constraintIdList as $indicatorId => $indicatorIdClass)
			{
				$deleteConstraintList[$indicatorId] = new $indicatorIdClass();
			}
		}

		/** @var \Bitrix\Disk\Volume\IDeleteConstraint $indicator */
		foreach ($deleteConstraintList as $indicatorId => $indicator)
		{
			if (!$indicator->isAllowDeleteFolder($folder))
			{
				$allowDrop = false;
			}
		}

		return $allowDrop;
	}

	/**
	 * Check ability to empty folder.
	 * @param \Bitrix\Disk\Folder $folder Folder to clear.
	 * @return boolean
	 */
	public function isAllowClearFolder(\Bitrix\Disk\Folder $folder)
	{
		$allowClear = true;

		if ($folder->isDeleted())
		{
			return true;
		}

		/** @var \Bitrix\Disk\Volume\IClearFolderConstraint[] $clearFolderConstraintList */
		static $clearFolderConstraintList;
		if (empty($clearFolderConstraintList))
		{
			$clearFolderConstraintList = array();

			// full list available indicators
			$constraintIdList = \Bitrix\Disk\Volume\Base::listClearFolderConstraint();
			foreach ($constraintIdList as $indicatorId => $indicatorIdClass)
			{
				$clearFolderConstraintList[$indicatorId] = new $indicatorIdClass();
			}
		}

		/** @var \Bitrix\Disk\Volume\IClearFolderConstraint $indicator */
		foreach ($clearFolderConstraintList as $indicatorId => $indicator)
		{
			if (!$indicator->isAllowClearFolder($folder))
			{
				$allowClear = false;
			}
		}

		return $allowClear;
	}

	/**
	 * Check ability to clear storage.
	 * @param \Bitrix\Disk\Storage $storage Storage to clear.
	 * @return boolean
	 */
	public function isAllowClearStorage(\Bitrix\Disk\Storage $storage)
	{
		$allowClear = true;

		/** @var \Bitrix\Disk\Volume\IClearConstraint[] $clearConstraintList */
		static $clearConstraintList;
		if (empty($clearConstraintList))
		{
			$clearConstraintList = array();

			// full list available indicators
			$constraintIdList = \Bitrix\Disk\Volume\Base::listClearConstraint();
			foreach ($constraintIdList as $indicatorId => $indicatorIdClass)
			{
				$clearConstraintList[$indicatorId] = new $indicatorIdClass();
			}
		}

		if ($storage instanceof \Bitrix\Disk\Storage)
		{
			/** @var \Bitrix\Disk\Volume\IClearConstraint $indicator */
			foreach ($clearConstraintList as $indicatorId => $indicator)
			{
				if (!$indicator->isAllowClearStorage($storage))
				{
					$allowClear = false;
				}
			}
		}

		return $allowClear;
	}

	/**
	 * Repeats measurement for indicator.
	 * @param Volume\IVolumeIndicator $indicator Ignited indicator for measure.
	 * @return boolean
	 */
	public static function repeatMeasure(Volume\IVolumeIndicator $indicator)
	{
		$indicator->resetMeasurementResult();
		$indicator->measure();

		if ($indicator->getFilterValue('STORAGE_ID') > 0)
		{
			if ($indicator::className() != Volume\Storage\Storage::className())
			{
				/** @var \Bitrix\Disk\Volume\IVolumeIndicator $storageIndicator */
				$storageIndicator = new Volume\Storage\Storage();
				$storageIndicator->setOwner($indicator->getOwner());

				$storageIndicator->addFilter('STORAGE_ID', $indicator->getFilterValue('STORAGE_ID'));
				$result = $storageIndicator->getMeasurementResult();
				if ($row = $result->fetch())
				{
					$storageIndicator->setFilterId($row['ID']);
				}
				$storageIndicator->measure();
			}

			if ($indicator::className() != Volume\Storage\TrashCan::className())
			{
				/** @var \Bitrix\Disk\Volume\IVolumeIndicator $trashCanIndicator */
				$trashCanIndicator = new Volume\Storage\TrashCan();
				$trashCanIndicator->setOwner($indicator->getOwner());

				$trashCanIndicator->addFilter('STORAGE_ID', $indicator->getFilterValue('STORAGE_ID'));
				$result = $trashCanIndicator->getMeasurementResult();
				if ($row = $result->fetch())
				{
					$trashCanIndicator->setFilterId($row['ID']);
				}
				$trashCanIndicator->measure();
			}
		}

		return true;
	}




	/**
	 * Gets dropped file count.
	 * @return int
	 */
	public function getDroppedFileCount()
	{
		return $this->instanceTask()->getDroppedFileCount();
	}

	/**
	 * Gets dropped version count.
	 * @return int
	 */
	public function getDroppedVersionCount()
	{
		return $this->instanceTask()->getDroppedVersionCount();
	}

	/**
	 * Gets dropped folder count.
	 * @return int
	 */
	public function getDroppedFolderCount()
	{
		return $this->instanceTask()->getDroppedFolderCount();
	}

	/**
	 * Gets task owner id.
	 * @return int
	 */
	public function getOwnerId()
	{
		return $this->ownerId;
	}

	/**
	 * Gets task owner.
	 * @return \Bitrix\Disk\User
	 */
	public function getOwner()
	{
		if (!($this->owner instanceof \Bitrix\Disk\User))
		{
			$this->owner = \Bitrix\Disk\User::loadById($this->getOwnerId());
			if (!($this->owner instanceof \Bitrix\Disk\User))
			{
				$this->owner = \Bitrix\Disk\User::loadById(\Bitrix\Disk\SystemUser::SYSTEM_USER_ID);
			}
		}

		return $this->owner;
	}


	/**
	 * Adds an array of errors to the collection.
	 * @param \Bitrix\Main\Error[] | \Bitrix\Main\Error $errors Raised error.
	 * @param boolean $increaseTaskFail Increase error count in task.
	 * @param boolean $raiseTaskFatalError Raise task fatal error.
	 * @return void
	 */
	public function collectError($errors, $increaseTaskFail = true, $raiseTaskFatalError = false)
	{
		if (!($this->errorCollection instanceof ErrorCollection))
		{
			$this->errorCollection = new ErrorCollection();
		}

		if (is_array($errors))
		{
			$this->errorCollection->add($errors);
			$lastError = array_pop($errors);
		}
		else
		{
			$this->errorCollection->add(array($errors));
			$lastError = $errors;
		}

		if (($this->task instanceof Volume\Task) && ($lastError instanceof Error))
		{
			if ($increaseTaskFail)
			{
				$this->instanceTask()->increaseFailCount();
			}
			if ($raiseTaskFatalError)
			{
				$this->instanceTask()->raiseFatalError();
			}
			$this->instanceTask()->setLastError($lastError->getMessage());
		}
	}

	/**
	 * @return Error[]
	 */
	public function getErrors()
	{
		if ($this->errorCollection instanceof ErrorCollection)
		{
			return $this->errorCollection->toArray();
		}

		return array();
	}

	/**
	 * @return boolean
	 */
	public function hasErrors()
	{
		if ($this->errorCollection instanceof ErrorCollection)
		{
			return $this->errorCollection->hasErrors();
		}

		return false;
	}

	/**
	 * Returns array of errors with the necessary code.
	 * @param string $code Code of error.
	 * @return Error[]
	 */
	public function getErrorsByCode($code)
	{
		if ($this->errorCollection instanceof ErrorCollection)
		{
			return $this->errorCollection->getErrorsByCode($code);
		}

		return array();
	}

	/**
	 * Returns an error with the necessary code.
	 * @param string|int $code The code of the error.
	 * @return \Bitrix\Main\Error|null
	 */
	public function getErrorByCode($code)
	{
		if ($this->errorCollection instanceof ErrorCollection)
		{
			return $this->errorCollection->getErrorByCode($code);
		}

		return null;
	}


	/**
	 * Set up information showing at stepper progress bar.
	 * @param int $ownerId Whom will mark as deleted by.
	 * @return array|null
	 */
	public static function getProgressInfo($ownerId)
	{
		$optionSerialized = \Bitrix\Main\Config\Option::get(
			'main.stepper.disk',
			Volume\Cleaner::className(). $ownerId,
			''
		);
		if (!empty($optionSerialized))
		{
			return unserialize($optionSerialized);
		}

		return null;
	}

	/**
	 * Set up information showing at stepper progress bar.
	 * @param int $ownerId Whom will mark as deleted by.
	 * @param int $totalFilesToDrop  Total files to drop.
	 * @param int $droppedFilesCount Dropped files count.
	 * @param int $failCount Failed deletion count.
	 * @return void
	 */
	public static function setProgressInfo($ownerId, $totalFilesToDrop, $droppedFilesCount = 0, $failCount = 0)
	{
		if ($totalFilesToDrop  > 0)
		{
			$option = Volume\Cleaner::getProgressInfo($ownerId);
			if (!empty($option) && $option['count'] > 0)
			{
				$prevTotalFilesToDrop = $option['count'];
				//$prevDroppedFilesCount = $option['steps'];

				// If total count decreases mean some agents finished its work.
				if ($prevTotalFilesToDrop > $totalFilesToDrop)
				{
					$droppedFilesCount = ($prevTotalFilesToDrop - $totalFilesToDrop) + $droppedFilesCount;
					$totalFilesToDrop = $prevTotalFilesToDrop;
				}
			}

			\Bitrix\Main\Config\Option::set(
				'main.stepper.disk',
				self::className().$ownerId,
				serialize(array('steps' => ($droppedFilesCount + $failCount), 'count' => $totalFilesToDrop))
			);
		}
		else
		{
			self::clearProgressInfo($ownerId);
		}
	}

	/**
	 * Remove stepper progress bar.
	 * @param int $ownerId Whom will mark as deleted by.
	 * @return void
	 */
	public static function clearProgressInfo($ownerId)
	{
		\Bitrix\Main\Config\Option::delete(
			'main.stepper.disk',
			array('name' => self::className(). $ownerId)
		);
	}
}