foldershare-8.x-1.2/src/Entity/FolderShareTraits/OperationDeleteTrait.php
src/Entity/FolderShareTraits/OperationDeleteTrait.php
<?php namespace Drupal\foldershare\Entity\FolderShareTraits; use Drupal\Component\Utility\Environment; use Drupal\foldershare\ManageLog; use Drupal\foldershare\Settings; use Drupal\foldershare\Utilities\CacheUtilities; use Drupal\foldershare\Utilities\LimitUtilities; use Drupal\foldershare\FolderShareInterface; use Drupal\foldershare\Entity\FolderShare; use Drupal\foldershare\Entity\FolderShareScheduledTask; use Drupal\foldershare\Entity\Exception\LockException; use Drupal\foldershare\Entity\Exception\ExecutionTimeLimitException; use Drupal\foldershare\Entity\Exception\MemoryLimitException; /** * Delete FolderShare entities. * * <B>Internal trait</B> * This trait is internal to the FolderShare module and used to define * features of the FolderShare entity class. It is a mechanism to group * functionality to improve code management. * * @ingroup foldershare */ trait OperationDeleteTrait { /*--------------------------------------------------------------------- * * Delete. * *---------------------------------------------------------------------*/ /** * {@inheritdoc} * * @internal * Deletion treats files and folders differently. * * For a file, image, or media kind, the entity is deleted immediately, * followed by deleting the underlying File or Media entity. * * For a folder, deletion proceeds in phases: * * - Phase 0. The folder to delete is marked as hidden. Since hidden items * are not shown in file/folder lists, this provides quick feedback to the * user that the item is being deleted. * * - Phase 1. A task is queued to sweep through all descendants and mark * them as hidden. For speed, this is done using direct database updates * rather than entity load/lock/set/save/unlock. This provides quick * feedback to all users viewing anything in the folder tree being deleted. * * - Phase 2. A task is queued to recurse through all descendants and delete * them. This takes longer because of the additional work to delete the * entity out of one or more tables, and service hooks associated with * deletion. * * Phases 0 executes in the requesting process. Phases 1 and 2 execute in * whatever process gets to the scheduled task. This is unlikely to be the * original requesting process, or even a process for the same user. */ public function delete () { // ------------------------------------------------------------------ // This item can be: // - A file or folder. // - At the root level or in a subfolder. // - Owned by the current user or another. // // Special cases: // - If the item is owned by another user, it is still deleted. It is up // to the caller to have checked permissions to insure this is valid. // // Errors: // - The item is new and has no ID yet. // // Actions: // - If the item is a file: Lock its root, delete, update ancestor sizes, // and unlock. // // - If the item is a folder: Lock its root, mark as hidden, clear access // grants (if it is a root), update ancestor sizes, and schedule "hide" // task. The "hide" task recursively marks descendants hidden, // flushes the cache, and schedules the "delete" task. The "delete" // task recursively deletes, and unlocks the root. // ------------------------------------------------------------------. if ( $this ->isNew() === TRUE) { // The has not been fully created yet (i.e. it is marked as "new"). // New items don't have an ID yet, and therefore can't have any // subfolders or files yet, so really there's nothing to delete. return ; } // // Lock root's folder tree. // ------------------------ // During a delete, all other operations that might interfere with the // delete must be blocked. And the delete cannot be allowed to interfere // with any other operation already started. // // LOCK ROOT FOLDER TREE. $rootId = $this ->getRootItemId(); if (self::acquireRootOperationLock( $rootId ) === FALSE) { throw new LockException( self::getStandardLockExceptionMessage(t( 'deleted' ), $this ->getName())); } // // Delete file or empty folder. // ---------------------------- // When the item is a folder without children or a file, there are no // children to recurse through and deletion is fairly quick. Delete the // item immediately and return. if ( $this ->findNumberOfChildren() === 0) { try { // Passing TRUE as arg1 to deleteInternal causes ancestor sizes to // be updated after the item is deleted. Deletion automatically // flushes deleted entities from all caches. $opCounter = 0; $this ->deleteInternal(TRUE, $opCounter , TRUE, (-1)); } catch (ExecutionTimeLimitException | MemoryLimitException $e ) { // An execution time or memory limit has been exceeded. // // This is VERY unlikely. We're deleting a file or empty folder. // Only a few entities have been loaded and that doesn't take // much time or space. // // The exception is thrown AFTER the item has been deleted, so // just continue. All that's left to do is release a lock. } catch (\Exception $e ) { // deleteInternal does not throw any known exceptions. // If it gets one, it has already been logged. // // UNLOCK ROOT FOLDER TREE. self::releaseRootOperationLock( $rootId ); throw $e ; } // UNLOCK ROOT FOLDER TREE. self::releaseRootOperationLock( $rootId ); return ; } // // Delete folder with children. // ---------------------------- // When the item is a folder, there may be children, who may have children, // and so on in a potentially huge folder tree. Without issuing queries, // we cannot know right now how big that folder tree is. We must assume // it is big enough that trying to delete it immediately would cause a // significant delay before responding to the user's request. // // To give quick feedback, we mark the item to be deleted as hidden. // This removes it from file/folder lists and looks like the item is gone. // This takes much less time than recursing through the children. // // Afterwards, we schedule a task to mark the folder's descendants. This // takes much less time than deleting those same descendants because it // does not require entity deletion, field and entity hooks, and logging. // // Only after that is done do we schedule a task to actually delete the // descendants. $this ->setSystemHidden(TRUE); $this ->clearAccessGrants(self::ANY_USER_ID, FALSE); $this ->save(); // // Update ancestor sizes. // ---------------------- // If needed, update ancestor sizes. if ( $this ->isRootItem() === FALSE && $this ->getSize() > 0) { $parent = $this ->getParentFolder(); if ( $parent !== NULL) { $parent ->updateSizeAndAncestors(); unset( $parent ); } } // // Mark and delete descendants. // ---------------------------- // First, mark all descendants as hidden. Then delete them in a // second pass. // // If we have time left before we need to respond to the user, start // the work. Otherwise schedule a task to do the work in the future. // // Keep root folder tree locks: // // - The root of the folder tree where content is being deleted. // // This will be unlocked by a future task when the entire delete is done. $requestingUid = (int) \Drupal::currentUser()->id(); $parameters = [ 'deleteIds' => [(int) $this ->id()], 'unlockRootId' => $rootId , ]; $started = time(); $comments = 'Start delete phase 1 (hide)' ; $executionTime = 0; if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) { self::processTaskDelete1( $requestingUid , $parameters , $started , $comments , $executionTime , TRUE); } else { FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskInitialDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskDelete1' , $requestingUid , $parameters , $started , $comments , $executionTime ); } } /** * Deletes multiple items and their descendants. * * For any item that is a file, image, or media item, the underlying File * or Media entity is deleted along with the item. Items are marked as * hidden while they are being deleted. This removes them from the * user interface. * * For any item that is a folder, a recursive traversal of the folder's * children deletes them first, followed by deletion of the folder itself. * Folders are marked as hidden as they are encountered during recursion. Root * folders have access grants cleared (including for the owner), which * blocks all user access to all descendants during deletion. * * System hidden and disabled items can be deleted. * * <B>Background delete</B> * File deletion occurs immediately, but folder deletion schedules background * tasks to traverse the folder tree and delete descendants. This will * delay completion of the delete to a time in the future that depends upon * the size of the folder tree being deleted and server load. * * <B>Post-operation hooks</B> * This method calls the "hook_foldershare_post_operation_delete" hook as * items are deleted. When deletion requires scheduled background tasks, * deletion hooks are called as those tasks are serviced. * * <B>Process locks</B> * This method groups items to delete by their root folder trees, then * processes each group. Processing locks each group's root folder tree * for exclusive use during the delete. This will prevent any other edit * operation from being performed on the same root folder tree until the * deletion completes. When deletion requires scheduled background tasks, * unlocking the root folder tree does not occur until the last descendant * is deleted. * * <B>Activity log</B> * This method posts a log message each time an item is deleted. When * deletion requires scheduled background tasks, log messages are posted * as those tasks are serviced. * * @param int[] $ids * An array of integer FolderShare entity IDs to delete. Invalid IDs * are silently skipped. * * @throws \Drupal\foldershare\Entity\Exception\LockException * If one or more items are in use and could not be locked and deleted. * * @see ::isSystemHidden() * @see ::delete() */ public static function deleteMultiple( array $ids ) { // ------------------------------------------------------------------ // Each item can be: // - A file or folder. // - At the root level or in a subfolder. // - Owned by the current user or another. // // Special cases: // - If an item is owned by another user, it is still deleted. It is up // to the caller to have checked permissions to insure this is valid. // // Errors: // - An item is new and has no ID yet. // // Actions: // - All items are sorted into groups with a shared root. In typical use, // there will be just one shared root. // // - For all files and folders in the same root group: Lock the shared // root. For each file, delete and update ancestor sizes. For each // folder, mark as hidden, clear access grants, and update ancestor sizes. // If there are no folders in the groupo, unlock the shared root. // // - After all groups: Schedule "hide" task. The "hide" task recursivelyw // marks descendants hidden, flushes the cache, and schedules the // "delete" task. The "delete" task recursively deletes, and unlocks // the shared root. // ------------------------------------------------------------------. if ( empty ( $ids ) === TRUE) { // Nothing to do. return ; } if ( count ( $ids ) === 1) { // Save some work and use the simpler method. $item = self::load( array_shift ( $ids )); if ( $item === NULL) { // The item does not exist. return ; } $item -> delete (); unset( $item ); return ; } // // Group IDs by root. // ------------------ // The IDs given could be from scattered locations. Group them by // their roots so that root folder locks can be done efficiently. // // Along the way also note if they are files or folders and ignore new // items. $rootGroups = []; foreach ( $ids as $id ) { $item = self::load( $id ); if ( $item === NULL || $item ->isNew() === TRUE) { // The item does not exist or it is new and has no ID yet. continue ; } $rootId = $item ->getRootItemId(); $kind = ( $item ->isFolder() === TRUE) ? 'folder' : 'file' ; $rootGroups [ $rootId ][ $kind ][] = $id ; unset( $item ); } unset( $ids ); if ( empty ( $rootGroups ) === TRUE) { // Nothing to do. return ; } // // Loop over root groups. // ---------------------- // Each root requires its own root folder lock, followed by deleting files // immediately and marking folders hidden and scheduling a task to delete // them and their descendants. // // This follows a pattern similar to delete(). See its comments for // details. $nLockExceptions = 0; $nRootGroups = count ( $rootGroups ); foreach ( $rootGroups as $rootId => $kinds ) { // LOCK ROOT FOLDER TREE. if (self::acquireRootOperationLock( $rootId ) === FALSE) { ++ $nLockExceptions ; continue ; } $fileIds = []; $folderIds = []; if (isset( $kinds [ 'file' ]) === TRUE) { $fileIds = $kinds [ 'file' ]; } if (isset( $kinds [ 'folder' ]) === TRUE) { $folderIds = $kinds [ 'folder' ]; } $rootGroups [ $rootId ] = NULL; // // Delete files. // ------------- // When items are files, there are no children to recurse through // and deletion is fairly quick. Delete the file immediately and return. foreach ( $fileIds as $id ) { $item = self::load( $id ); if ( $item === NULL) { // The item does not exist. continue ; } try { // Passing TRUE to arg1 of deleteInternal causes ancestor sizes to // be updated after the item is deleted. Deletion automatically // flushes the item from all caches. $opCounter = 0; $item ->deleteInternal(TRUE, $opCounter , TRUE, (-1)); unset( $item ); } catch (ExecutionTimeLimitException | MemoryLimitException $e ) { // An execution time or memory limit has been exceeded. // // This is VERY unlikely. We're deleting a file. // Only a few entities have been loaded and this doesn't take // much time or space. // // The exception is thrown AFTER the item has been deleted, so // just continue and hope for the best. } catch (\Exception $e ) { // deleteInternal does not throw any known exceptions. // If it gets one, it has already been logged. } } unset( $fileIds ); if ( empty ( $folderIds ) === TRUE) { // There are no folders to delete. Release the lock and move on. // // UNLOCK ROOT FOLDER TREE. self::releaseRootOperationLock( $rootId ); continue ; } // // Delete folders. // --------------- // When items are folders, there may be children, who may have children, // and so on in a potentially huge folder tree. Without issuing queries, // we cannot know right now how big that folder tree is. We must assume // it is big enough that trying to delete it immediately would cause a // significant delay before responding to the user's request. // // To give quick feedback, mark the item to be deleted as hidden. // This removes it from file/folder lists and looks like the item is gone. // This takes much less time than recursing through the children. // // Afterwards, schedule a task to mark the folder's descendants. This // takes much less time than deleting those same descendants because it // does not require entity deletion, field and entity hooks, and logging. // // Only after that is done do we schedule a task to actually delete the // descendants. $deleteIds = []; foreach ( $folderIds as $id ) { $item = self::load( $id ); if ( $item === NULL) { // The item does not exist. continue ; } $item ->setSystemHidden(TRUE); $item ->clearAccessGrants(self::ANY_USER_ID, FALSE); $item ->save(); $deleteIds [] = (int) $item ->id(); // // Update ancestor sizes. // ---------------------- // If needed, update ancestor sizes. if ( $item ->isRootItem() === FALSE && $item ->getSize() > 0) { $parent = $item ->getParentFolder(); if ( $parent !== NULL) { $parent ->updateSizeAndAncestors(); unset( $parent ); } } unset( $item ); } unset( $folderIds ); // Garbage collect. Deletion has loaded and released multiple entities. // Flush them from memory ASAP. gc_collect_cycles(); // // Mark and delete descendants. // ---------------------------- // First, mark all descendants as hidden. Then delete them in a // second pass. // // If we have one root group only and we have time left before we // need to respond to the user, start the work. Otherwise schedule // a task to do the work in the future. // // Keep root folder tree locks: // // - The root of the folder tree where content is being deleted. // // This will be unlocked by a future task when the entire delete is done. $requestingUid = (int) \Drupal::currentUser()->id(); $parameters = [ 'deleteIds' => $deleteIds , 'unlockRootId' => $rootId , ]; $started = time(); $comments = 'Start delete phase 1 (hide)' ; $executionTime = 0; if ( $nRootGroups === 1 && LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) { self::processTaskDelete1( $requestingUid , $parameters , $started , $comments , $executionTime , TRUE); } else { FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskInitialDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskDelete1' , $requestingUid , $parameters , $started , $comments , $executionTime ); } } if ( $nLockExceptions !== 0) { throw new LockException( self::getStandardLockExceptionMessage(t( 'deleted' ), NULL)); } } /** * Implements item deletion. * * <B>This method is internal and strictly for use by the FolderShare * module itself.</B> * * The caller MUST have locked the root folder tree. * * If this item is a file, image, or media item, it is deleted immediately * along with its wrapped File or Media entity. * * If this item is a folder, recursion loops downward through subfolders. * Folders are marked as "hidden" as they are encountered. * * <B>Post-operation hooks</B> * This method calls the "hook_foldershare_post_operation_delete" hook. * * <B>Activity log</B> * This method posts a log message after each item is deleted. * * <B>Process locks</B> * This method does not lock anything. The caller MUST have locked the * root folder tree. * * @param bool $updateAncestorSizes * (optional, default = FALSE) When TRUE, the item's ancestor folder * sizes are updated after an item is deleted. During recursion, this * is always FALSE. * @param int $opCounter * A counter that increments each time a load, save, or delete is done, * and is reset to zero each time memory and execution time limits are * checked. * @param bool $interactive * (optional, default = FALSE) When TRUE, this task is executing in a * direct response to a user request that is still in progress, and it * should therefore return fairly quickly. When FALSE, this task is * executing as a background task and it can take longer without * impacting interactivity. * @param int $requestingUid * (optional, default = current user) The user ID of the user requesting * the operation. When interactive, this is the current user. When this * is a background task, this is the original requesting user. * * @throws \Drupal\foldershare\Entity\Exception\ExecutionTimeLimitException * Throws an exception if the execution time has reached its limit. * @throws \Drupal\foldershare\Entity\Exception\MemoryLimitException * Throws an exception if memory use has reached its limit. */ private function deleteInternal( bool $updateAncestorSizes , int & $opCounter , bool $interactive = FALSE, int $requestingUid = (-1)) { if ( $requestingUid < 0) { $requestingUid = self::getCurrentUserId()[0]; } if ( $this ->isFolder() === FALSE) { $this ->deleteInternalFile( $updateAncestorSizes , $opCounter , $requestingUid ); } else { $this ->deleteInternalFolder( $updateAncestorSizes , $opCounter , $interactive , $requestingUid ); } // Garbage collect. Deletion has deleted multiple objects. Flush them // from memory ASAP. gc_collect_cycles(); if ( $opCounter >= self::USAGE_CHECK_INTERVAL) { if (( $interactive === TRUE && LimitUtilities::aboveResponseExecutionTimeLimit() === TRUE) || LimitUtilities::aboveExecutionTimeLimit() === TRUE) { // Execution time limit has been reached. throw new ExecutionTimeLimitException(); } if (LimitUtilities::aboveMemoryUseLimit() === TRUE) { // Memory usage limit has been reached. throw new MemoryLimitException(); } $opCounter = 0; } } /** * Implements file deletion. * * <B>This method is internal and strictly for use by the FolderShare * module itself.</B> * * The caller MUST have locked the root folder tree. * * See deleteInternal() for discussion of deletion, hooks, and logging. * * This method only handles deletion of file, image, or media FolderShare * entities. It is presumed that the caller has already checked that the * entity is one of these before calling this method. * * <B>Process locks</B> * This method does not lock anything. The caller MUST have locked the * root folder tree. * * @param bool $updateAncestorSizes * (optional, default = FALSE) When TRUE, the item's ancestor folder * sizes are updated after an item is deleted. During recursion, this * is always FALSE. * @param int $opCounter * A counter that increments each time a load or delete is done, and is * reset to zero each time memory and execution time limits are checked. * @param int $requestingUid * The user ID of the user requesting the operation. */ private function deleteInternalFile( bool $updateAncestorSizes , int & $opCounter , int $requestingUid ) { // // Setup. // ------ // Pull out some values we need below. $thisId = $this ->id(); $thisName = $this ->getName(); $thisKind = $this ->getKind(); $isRoot = $this ->isRootItem(); $parentId = $this ->getParentFolderId(); $size = $this ->getSize(); $thisPath = '' ; if (ManageLog::isActivityLoggingEnabled() === TRUE) { $thisPath = $this ->getPath(); } $file = $this ->getFile(); $image = $this ->getImage(); $media = $this ->getMedia(); try { // // Delete. // ------- // Let the parent class delete the object. Deletion automatically // removes the item from all caches. parent:: delete (); // Increment to count delete. ++ $opCounter ; // // Delete wrapped file or media. // ----------------------------- // Force deletion of the file/media since it is supposed to only be // referenced by the wrapper we just deleted. // // These may be redundant if these have already been deleted. if ( $file !== NULL) { // Delete file. Deletion automatically removes the item from // all caches. $file -> delete (); unset( $file ); // Increment to count delete. ++ $opCounter ; } if ( $image !== NULL) { // Delete file. Deletion automatically removes the item from // all caches. $image -> delete (); unset( $image ); // Increment to count delete. ++ $opCounter ; } if ( $media !== NULL) { // Delete media. Deletion automatically removes the item from // all caches. $media -> delete (); unset( $media ); // Increment to count delete. ++ $opCounter ; } } catch (\Exception $e ) { // Unknown exception. Continue and assume the item is really deleted. ManageLog::exception( $e , $requestingUid ); } // // Hook & log. // ----------- // Announce the deletion. self::postOperationHook( 'delete' , [ $thisId , $requestingUid , ]); ManageLog::activity( "Deleted @kind '@name' (# @id).\nPath: @path" , [ '@id' => $thisId , '@kind' => $thisKind , '@name' => $thisName , '@path' => $thisPath , 'uid' => $requestingUid , ]); // // Update ancestor sizes. // ---------------------- // If needed, update ancestor sizes. if ( $updateAncestorSizes === TRUE && $isRoot === FALSE && $size > 0) { $parent = FolderShare::load( $parentId ); if ( $parent !== NULL) { $parent ->updateSizeAndAncestors(); unset( $parent ); // Increment to count parent load. // // This really should be incremented by the number of ancestors // changed, but we don't know that. ++ $opCounter ; } } } /** * Implements recursive folder deletion. * * <B>This method is internal and strictly for use by the FolderShare * module itself.</B> * * The caller MUST have locked the root folder tree. * * See deleteInternal() for discussion of deletion, hooks, and logging. * * This method only handles deletion of folders. It is presumed that the * caller has already checked that the entity is a folder before calling * this method. * * <B>Process locks</B> * This method does not lock anything. The caller MUST have locked the * root folder tree. * * @param bool $updateAncestorSizes * When TRUE, the item's ancestor folder sizes are updated after an * item is deleted. During recursion, this is always FALSE. * @param int $opCounter * A counter that increments each time a load or copy is done, and is * reset to zero each time memory and execution time limits are checked. * @param bool $interactive * When TRUE, this task is executing in a direct response to a user * request that is still in progress, and it should therefore return * fairly quickly. When FALSE, this task is executing as a background * task and it can take longer without impacting interactivity. * @param int $requestingUid * The user ID of the user requesting the operation. * * @throws \Drupal\foldershare\Entity\Exception\ExecutionTimeLimitException * Throws an exception if the execution time has reached its limit. * @throws \Drupal\foldershare\Entity\Exception\MemoryLimitException * Throws an exception if memory use has reached its limit. */ private function deleteInternalFolder( bool $updateAncestorSizes , int & $opCounter , bool $interactive , int $requestingUid ) { // // Mark as hidden and clear access. // -------------------------------- // Marking the folder hidden makes it invisible to users. When invoked // from the phase 2 delete task, phase 1 has already recursed through // folders and marked everything hidden. So check first before doing // it again. // // For root items, clearing access grants (for the owner too) blocks // all access to the folder tree under the root. // // Then SAVE the folder. It is not deleted yet. We can't delete it until // we've deleted the children. if ( $this ->isSystemHidden() === FALSE || $this ->areAccessGrantsCleared() === FALSE) { $this ->setSystemHidden(TRUE); $this ->clearAccessGrants(self::ANY_USER_ID, FALSE); $this ->save(); // Increment to count save. ++ $opCounter ; } // // Update ancestor sizes. // ---------------------- // If needed, update ancestor sizes. if ( $updateAncestorSizes === TRUE && $this ->isRootItem() === FALSE && $this ->getSize() > 0) { $parent = $this ->getParentFolder(); if ( $parent !== NULL) { $parent ->updateSizeAndAncestors(); unset( $parent ); // Increment to count parent load. // // This really should be incremented by the number of ancestors // changed, but we don't know that. ++ $opCounter ; } } // // Delete children. // ---------------- // Recurse to delete children. Find all children, including disabled // and hidden children (actually, all children during a delete should // have been marked hidden). $childIds = $this ->findChildrenIds(TRUE, TRUE); foreach ( $childIds as $childId ) { $child = self::load( $childId ); if ( $child === NULL) { // The child has already been deleted. This can occur if another // process did the delete first. Silently skip the child. continue ; } // Increment to count child load. ++ $opCounter ; // Recurse to delete the child. Deletion automatically removes the // item from all caches. // // Below throws an exception when memory use or execution // time exceed a limit. Let that exception propagate up to the caller // through any number of recursion levels. $child ->deleteInternal(FALSE, $opCounter , $interactive , $requestingUid ); unset( $child ); } unset( $childIds ); // // Delete. // ------- // Let the parent class finish deleting the object. There are no more // children, so it is now safe to delete the folder. $thisId = $this ->id(); $thisName = $this ->getName(); $thisKind = $this ->getKind(); $thisPath = '' ; if (ManageLog::isActivityLoggingEnabled() === TRUE) { $thisPath = $this ->getPath(); } try { // Delete this. Deletion automatically removes the item from all caches. parent:: delete (); // Increment to count delete. ++ $opCounter ; } catch (\Exception $e ) { ManageLog::exception( $e , $requestingUid ); } // // Hook & log. // ----------- // Announce the deletion. self::postOperationHook( 'delete' , [ $thisId , $requestingUid , ]); ManageLog::activity( "Deleted @kind '@name' (# @id).\nPath: @path" , [ '@id' => $thisId , '@kind' => $thisKind , '@name' => $thisName , '@path' => $thisPath , 'uid' => $requestingUid , ]); // Garbage collect. Deletion has removed multiple objects. Flush them // from memory ASAP. gc_collect_cycles(); } /*--------------------------------------------------------------------- * * Delete all. * *---------------------------------------------------------------------*/ /** * Deletes all items, or just those owned by a user. * * <B>This method is intended for use by site administrators.</B> * It deletes all items, or a specific user's items, and can be used * as part of deleting a user's account or prior to uninstalling the module. * * With the exception of root-level files, this method does not delete * items immediately. Instead it schedules a task to traverse through * folder trees and delete descendants. This is necessary because the * number of items to delete may be very large and the time required may * exceed the execution time limits of PHP and the web server. Callers * should therefore not assume that all of the user's content has been * deleted when this method returns. * * <B>Background delete</B> * File deletion occurs immediately, but folder deletion schedules background * tasks to traverse the folder tree and delete descendants. This will * delay completion of the delete to a time in the future that depends upon * the size of the folder tree being deleted and server load. * * <B>Post-operation hooks</B> * This method calls the "hook_foldershare_post_operation_delete" hook as * items are deleted. When deletion requires scheduled background tasks, * deletion hooks are called as those tasks are serviced. * * <B>Process locks</B> * This method groups items to delete by their root folder trees, then * processes each group. Processing locks each group's root folder tree * for exclusive use during the delete. This will prevent any other edit * operation from being performed on the same root folder tree until the * deletion completes. When deletion requires scheduled background tasks, * unlocking the root folder tree does not occur until the last descendant * is deleted. * * <B>Activity log</B> * This method posts a log message each time an item is deleted. When * deletion requires scheduled background tasks, log messages are posted * as those tasks are serviced. * * @param int $uid * (optional, default = FolderShareInterface::ANY_USER_ID) The user ID * of the user for whome to delete all content. If the user ID is * FolderShareInterface::ANY_USER_ID or negative, all content is * deleted for all users. * * @see ::delete() * * @internal * It is tempting to just truncate the folder table, but this * doesn't give other modules a chance to clean up in response to hooks. * The primary relevant module is the File module, which keeps reference * counts for files. But other modules may have added fields to * folders, and they too need a graceful way of handling deletion. * So, this method must recurse and delete each item intentionally. */ public static function deleteAll(int $uid = self::ANY_USER_ID) { // ------------------------------------------------------------------ // Special cases: // - If the $uid is ANY_USER_ID, then the query of roots below gets // everything. There is no second step. // // Actions: // - A list of all root items owned by the user (or by all users) is // queried and one by one passed to delete(). // // - A list of all non-root items in root folder trees not owned by // the user is queried and passed to deleteMultiple(). // ------------------------------------------------------------------. // // Prepare. // -------- // Try to push the PHP timeout to "unlimited" so that a long operation // has a better chance to complete. This may not work if the site has // PHP timeout changes blocked and it does nothing to stop web server // timeouts. Environment::setTimeLimit(0); // // Delete roots owned by user (or all users). // ------------------------------------------ // Get a list of all root items owned by the user (or all users) // and delete them one by one. // // While we could pass all of these to deleteMultiple(), it wouldn't // save any time. deleteMultiple() would have to load them all, find // that they are all different roots, and then go through the same // work as delete(). So we just call delete() and void the extra work. $rootIds = self::findAllRootItemIds( $uid ); $nLockExceptions = 0; if ( empty ( $rootIds ) === FALSE) { foreach ( $rootIds as $rootId ) { $item = self::load( $rootId ); if ( $item === NULL) { // The item does not exist. continue ; } try { // Delete it. Deletion automatically flushes the item from // all caches. $item -> delete (); unset( $item ); } catch (LockException $e ) { ++ $nLockExceptions ; } // Garbage collect. We've just deleted multiple entities and their // objects. Flush them from memory ASAP. gc_collect_cycles(); } } if ( $uid === self::ANY_USER_ID) { // When deleting everything for ANY user, the above root ID list // already includes everything. There is nothing further to do. if ( $nLockExceptions !== 0) { throw new LockException( self::getStandardLockExceptionMessage(t( 'deleted' ), NULL)); } return ; } // // Delete misc owned by user. // -------------------------- // Get a list of items that are not roots and not within the folder // trees of the user's roots. These will be individual files and folders // buried within the folder trees of other users, such as shared content. // // Get a list of all root IDs for roots that are NOT owned by the user. $otherRootIds = array_diff ( self::findAllRootItemIds(FolderShareInterface::ANY_USER_ID), $rootIds ); if ( empty ( $otherRootIds ) === TRUE) { // There are no other root folder trees. Nothing more to delete. if ( $nLockExceptions !== 0) { throw new LockException( self::getStandardLockExceptionMessage(t( 'deleted' ), NULL)); } return ; } // For each root NOT owned by the user, find anything within the root's // folder tree that IS owned by the user. Delete it. When it is a folder, // this will delete its contents too. Not much else we can do. foreach ( $otherRootIds as $rootId ) { $item = self::load( $rootId ); if ( $item === NULL) { // The item does not exist. continue ; } $descendantIds = $item ->findDescendantIdsByOwnerId( $uid ); if ( empty ( $descendantIds ) === FALSE) { // Delete all of them. Deletion automatically removes items from // all caches. self::deleteMultiple( $descendantIds ); } unset( $descendantIds ); unset( $item ); // Garbage collect. We've just deleted multiple entities and their // objects. Flush them from memory ASAP. gc_collect_cycles(); } if ( $nLockExceptions !== 0) { throw new LockException( self::getStandardLockExceptionMessage(t( 'deleted' ), NULL)); } } /*--------------------------------------------------------------------- * * Background task handling. * *---------------------------------------------------------------------*/ /** * Processes a scheduled phase 1 delete task to mark descendants hidden. * * <B>This method is internal and strictly for use by the FolderShare * module itself.</B> This method is public so that it can be called * from the module's scheduled task handler. * * A delete task has a list of IDs for entities to delete. Phase 1, * implemented here, recurses through all descendants to quickly mark * them hidden. This is done with direct database updates, rather than * entity loads, locks, sets, saves, and unlocks in order to mark items * quickly throughout a potentially large folder tree. * * When phase 1 is complete, this method schedules a phase 2 task to * go back through the descendants and delete them. * * <B>Process locks</B> * This method does not acquire or release any locks. * * <B>Post-operation hooks</B> * This method does not call hooks. * * <B>Activity log</B> * This method does not log activity. * * @param int $requestingUid * The user ID of the user that requested the delete. This is ignored. * @param array $parameters * The queued task's parameters. This is an associative array with keys: * - 'deleteIds': the IDs of items to be deleted, recursively. * - 'unlockRootId': the ID of the root to unlock upon completion. * @param int $started * The timestamp of the start date & time for an operation that causes * a chain of tasks. * @param string $comments * A comment on the current task. * @param int $executionTime * The accumulated total execution time of the task chain, in seconds. * @param bool $interactive * (optional, default = FALSE) When TRUE, this task is executing in a * direct response to a user request that is still in progress, and it * should therefore return fairly quickly. When FALSE, this task is * executing as a background task and it can take longer without * impacting interactivity. * * @see ::delete() */ public static function processTaskDelete1( int $requestingUid , array $parameters , int $started , string $comments , int $executionTime , bool $interactive = FALSE) { // // Validate. // --------- // The parameters array must contain a list of entity IDs and the root ID // of the folder tree to unlock when the delete is done. if (isset( $parameters [ 'deleteIds' ]) === FALSE || is_array ( $parameters [ 'deleteIds' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , 'deleteIds' ); return ; } if (isset( $parameters [ 'unlockRootId' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , 'unlockRootIds' ); return ; } $deleteIds = $parameters [ 'deleteIds' ]; $unlockRootId = (int) $parameters [ 'unlockRootId' ]; // // Reschedule full task. // --------------------- // As a safety net, reschedule the entire task immediately. This insures // that if we get a PHP or web server timeout that interrupts the task, // it will be run again to try and complete it in the near future. $safetyNetTask = FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskSafetyNetDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskDelete1' , $requestingUid , [ 'deleteIds' => $deleteIds , 'unlockRootId' => $unlockRootId , ], $started , 'Safety-net requeue' , $executionTime ); // // Prepare. // -------- // Flush all FolderShare entities from caches everywhere. Below we // modify the database directly in order to set the hidden flag ASAP. // Once changed, all cached copies of modified items are out of date. // // We'd prefer to only flush cached items that are actually changed, // but getting such a list can be slow if the folder tree to delete // is large. And we don't know if it is large. The safe quick choice, // then, is to clear the entire cache. CacheUtilities::flushAllEntityCaches(self::ENTITY_TYPE_ID); $beginTime = time(); // // Mark descendants hidden. // ------------------------ // Use database updates to mark all descendants as hidden as quickly // as possible. // // For a huge folder tree, this may be interrupted by a PHP or web // server timeout. The safety net task scheduled above will try again. // Since the queries used skip subtrees that are already hidden, a // repeated run will find fewer things to change and go quicker. // Over repeated runs, this will eventually complete. foreach ( $deleteIds as $id ) { try { self::setDescendantsSystemHidden( $id ); } catch (\Exception $e ) { ManageLog::exception( $e , $requestingUid ); } } // Flush all FolderShare entities from caches everywhere. Above we // modified the database directly in order to set the hidden flag ASAP. // Once changed, all cached copies are out of date. CacheUtilities::flushAllEntityCaches(self::ENTITY_TYPE_ID); // // Delete descendants. // ------------------- // Now that all descendants are marked hidden, delete them. // // If we have time left before we need to respond to the user, start // the work. Otherwise schedule a task to do the work in the future. $parameters = [ 'deleteIds' => $deleteIds , 'unlockRootId' => $unlockRootId , ]; $comments = 'Start of delete phase 2 (delete)' ; $executionTime += (time() - $beginTime ); if (( $interactive === TRUE && LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) || LimitUtilities::aboveExecutionTimeLimit() === FALSE) { // Delete the safety net task. FolderShareScheduledTask::deleteTask( $safetyNetTask ); self::processTaskDelete2( $requestingUid , $parameters , $started , $comments , $executionTime , TRUE); } else { FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskContinuationDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskDelete2' , $requestingUid , $parameters , $started , $comments , $executionTime ); // Delete the safety net task. FolderShareScheduledTask::deleteTask( $safetyNetTask ); } } /** * Processes a scheduled phase 2 delete task to delete descendants. * * <B>This method is internal and strictly for use by the FolderShare * module itself.</B> This method is public so that it can be called * from the module's scheduled task handler. * * A delete task has a list of IDs for entities to delete. Deletion * recurses through folder entities, deleting child files before deleting * the parent folder. * * <B>Process locks</B> * This method releases the root folder tree lock acquired when the task * was started. * * <B>Post-operation hooks</B> * This method calls the "hook_foldershare_post_operation_deleted" hook for * each item deleted. * * <B>Activity log</B> * If the site has enabled logging of operations, this method posts a * log message for each item deleted. * * @param int $requestingUid * The user ID of the user that requested the delete. This is ignored. * @param array $parameters * The queued task's parameters. This is an associative array with keys: * - 'deleteIds': the IDs of items to be deleted, recursively. * - 'unlockRootId': the ID of the root to unlock upon completion. * @param int $started * The timestamp of the start date & time for an operation that causes * a chain of tasks. * @param string $comments * A comment on the current task. * @param int $executionTime * The accumulated total execution time of the task chain, in seconds. * @param bool $interactive * (optional, default = FALSE) When TRUE, this task is executing in a * direct response to a user request that is still in progress, and it * should therefore return fairly quickly. When FALSE, this task is * executing as a background task and it can take longer without * impacting interactivity. * * @see ::delete() */ public static function processTaskDelete2( int $requestingUid , array $parameters , int $started , string $comments , int $executionTime , bool $interactive = FALSE) { // // Validate. // --------- // The parameters array must contain a list of entity IDs. if (isset( $parameters [ 'deleteIds' ]) === FALSE || is_array ( $parameters [ 'deleteIds' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , 'deleteIds' ); return ; } if (isset( $parameters [ 'unlockRootId' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , 'unlockRootId' ); return ; } $deleteIds = $parameters [ 'deleteIds' ]; $unlockRootId = (int) $parameters [ 'unlockRootId' ]; // // Reschedule full task. // --------------------- // As a safety net, reschedule the entire task immediately. This insures // that if we get a PHP or web server timeout that interrupts the task, // it will be run again to try and complete it in the near future. $safetyNetTask = FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskSafetyNetDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskDelete2' , $requestingUid , [ 'deleteIds' => $deleteIds , 'unlockRootId' => $unlockRootId , ], $started , 'Safety-net requeue' , $executionTime ); // // Prepare. // -------- // Garbage collect and initialize. $beginTime = time(); $opCounter = 0; gc_collect_cycles(); // // Delete descendants recursively. // ------------------------------- // Delete each item, if it hasn't already been deleted. foreach ( $deleteIds as $deleteIndex => $id ) { $item = FolderShare::load((int) $id ); if ( $item === NULL) { // The item has already been deleted by another process. continue ; } // Increment to count load. ++ $opCounter ; try { // Passing TRUE to below asks that it update parent folder // sizes. This is only needed for the top-most items being deleted. // Deletion removes all of the deleted entities from all caches. $item ->deleteInternal(TRUE, $opCounter , $interactive , $requestingUid ); } catch (ExecutionTimeLimitException | MemoryLimitException $e ) { // An execution time or memory limit has been exceeded. // // This is our chance to gracefully handle a condition where // the execution time or memory use is reaching its configured // limits. If we do nothing, we will hit that limit and the // process will crash with a nasty message. The safety net task // will remain and be serviced by the next process and continue // the operation. But that nasty crash message will look bad // and worry admins. It could also have interrupted something // and left content in a corrupted state. // // Instead, when we near a limit, gracefully stop what we are // doing and return. We'll schedule a continuation task that // will be serviced by the next process and continue the operation. // // DO NOT release the final root lock. The task is not done yet. // // Schedule continuation task. Execution has already unset // entries in the $deleteIds array as they've been deleted. $reason = ( $e instanceof ExecutionTimeLimitException) ? 'time limit' : 'memory use limit' ; FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskContinuationDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskDelete2' , $requestingUid , [ 'deleteIds' => $deleteIds , 'unlockRootId' => $unlockRootId , ], $started , "Continuation due to $reason after $opCounter ops" , $executionTime + (time() - $beginTime )); // Delete the safety net task. FolderShareScheduledTask::deleteTask( $safetyNetTask ); return ; } unset( $item ); unset( $deleteIds [ $deleteIndex ]); } // UNLOCK ROOT FOLDER TREE. self::releaseRootOperationLock( $unlockRootId ); // Delete the safety net task. FolderShareScheduledTask::deleteTask( $safetyNetTask ); } } |