foldershare-8.x-1.2/src/Entity/FolderShareTraits/OperationMoveTrait.php
src/Entity/FolderShareTraits/OperationMoveTrait.php
<?php namespace Drupal\foldershare\Entity\FolderShareTraits; use Drupal\foldershare\ManageLog; use Drupal\foldershare\Settings; use Drupal\foldershare\Utilities\FormatUtilities; use Drupal\foldershare\Utilities\LimitUtilities; use Drupal\foldershare\FolderShareInterface; use Drupal\foldershare\Entity\FolderShareScheduledTask; use Drupal\foldershare\Entity\Exception\LockException; use Drupal\foldershare\Entity\Exception\ValidationException; /** * Move FolderShare entities. * * This trait includes methods to move FolderShare entities and place * them in a folder or at the root level. * * <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 OperationMoveTrait { /*--------------------------------------------------------------------- * * Move to root. * *---------------------------------------------------------------------*/ /** * {@inheritdoc} */ public function moveToRoot( string $newName = '' , bool $allowRename = FALSE) { // ------------------------------------------------------------------ // 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 already at the root level and no new name is given, // do nothing. // // - If the item is already at the root level and a new name is given, // redirect to rename(). // // - If the item is owned by another user, it is moved to the OWNER's // root list, not the current user's. It is up to the caller to have // decided if this is valid (admins can do this, while regular users // should not). // // Errors: // - The new name is illegal. // - The new name is in use in the root list and renaming is not allowed. // // Actions: // - If the item is a file: Lock item, lock original root, lock root list, // set name, clear parent and root, unlock root list, update old // parent size, unlock original root, unlock item. // // - If the item is a folder: Lock item, lock original root, lock root list, // set name, clear parent and root, clear access grants, set disabled, // unlock root list, update old parent size, unlock original root, & // schedule task. The task recurses through the item setting root IDs // then unlock item. // // ------------------------------------------------------------------ // // Validate. // --------- // If this item is already a root, then this is really a rename, // which is handled separately. if ( $this ->isRootItem() === TRUE) { if ( empty ( $newName ) === TRUE) { // Move to same location with same name. Do nothing. return ; } $this ->rename( $newName ); return ; } // // Check name legality. // -------------------- // If there is a new name, throw an exception if it is not suitable. if ( empty ( $newName ) === FALSE) { // The checkName() function throws an exception if the name is too // long or uses illegal characters. $this ->checkName( $newName ); } else { $newName = $this ->getName(); } // // Lock this item's folder tree AS IF it were a root. // -------------------------------------------------- // This item is about to be made a root. Before it is, lock it's // folder tree so that there is no gap between the move and locking it. // // Since this lock will be the last one to be released, it is also // important that it be the first to be acquired so that we don't get // race conditions. // // 1. LOCK THIS ITEM AS ROOT FOLDER TREE. if (self::acquireRootOperationLock( $this ->id()) === FALSE) { throw new LockException( self::getStandardLockExceptionMessage(t( 'moved' ), $this ->getName())); } // // Lock original root's folder tree. // --------------------------------- // The item is currently in another root's folder tree. Lock it so that // all other operations that might interfere with the move are blocked. // // 2. LOCK ORIGINAL ROOT FOLDER TREE. $originalRootId = $this ->getRootItemId(); if (self::acquireRootOperationLock( $originalRootId ) === FALSE) { // 1. UNLOCK THIS ITEM AS ROOT FOLDER TREE. self::releaseRootOperationLock( $this ->id()); throw new LockException( self::getStandardLockExceptionMessage(t( 'moved' ), $this ->getName())); } // // Lock the root list. // ------------------- // Lock the root list before checking if this item has a name collision // in the root list. // // Everything on a root list is owned by a user. It is not possible for // an item to be on one user's root list, but owned by another. This // means a move-to-root can only move the item to the item's OWNER's // root list. Moving it to the current user's root list, when the owner // is not the current user, would require changing ownership too. And // that is not what this method does. // // If moving an item owned by another to that owner's root list is not // what was intended, it should be detected and blocked before calling // this method. An admin, for instance, could reasonably be allowed to // move another user's file or folder to that user's root list. But a // normal user probably shouldn't be able to do that. // // 3. LOCK OWNER'S ROOT LIST. $ownerId = $this ->getOwnerId(); if (self::acquireUserRootListLock( $ownerId ) === FALSE) { // 2. UNLOCK ORIGINAL ROOT FOLDER TREE. self::releaseRootOperationLock( $originalRootId ); // 1. UNLOCK THIS ITEM AS ROOT FOLDER TREE. self::releaseRootOperationLock( $this ->id()); throw new LockException( self::getStandardLockExceptionMessage(t( 'moved' ), $this ->getName())); } // // Check name. // ----------- // If renaming is not allowed, check if the name is already in use in // the root list and abort if it is. // // If renaming is allowed, create a new unique name in the root list. if ( $allowRename === FALSE) { if ( empty (self::findAllRootItemIds( $ownerId , $newName )) === FALSE) { // 3. UNLOCK OWNER'S ROOT LIST. self::releaseUserRootListLock( $ownerId ); // 2. UNLOCK ORIGINAL ROOT FOLDER TREE. self::releaseRootOperationLock( $originalRootId ); // 1. UNLOCK THIS ITEM AS ROOT FOLDER TREE. self::releaseRootOperationLock( $this ->id()); throw new ValidationException( self::getStandardRenameFirstExceptionMessage( $newName )); } } else { $rootNames = self::findAllRootItemNames( $ownerId ); $newName = self::createUniqueName( $rootNames , $newName ); if ( $newName === FALSE) { // This is very very unlikely because creating a unique name tries // repeatedly to append a number until it gets to something unique. throw new ValidationException( self::getStandardCannotCreateUniqueNameExceptionMessage( 'move' )); } unset( $rootNames ); } // // Decide if a task will be needed. // -------------------------------- // If the item being moved is: // - A folder. // - With children. // // Then we'll need to schedule a task. $taskNeeded = ( $this ->isFolder() === TRUE && $this ->findNumberOfChildren() > 0); // // Update parent and root IDs. // --------------------------- // Update this item, clearing the parent and root IDs to make it a root. // Set the name (which may or may not be new). Clear access grants, but // leave defaults for the current owner. // // These changes, when saved, provide immediate feedback to the user. The // item will now show up in the user's root list and not in the file/folder // list of its old parent folder. $oldParentId = $this ->getParentFolderId(); $this ->clearParentFolderId(); $this ->clearRootItemId(); $this ->clearAccessGrants(); $this ->setSystemDisabled( $taskNeeded ); $this ->setName( $newName ); $this ->save(); // If this item is a file, image, or media object, change the underlying // item's name too. $this ->renameWrappedFile( $newName ); // // Unlock user's root list. // ------------------------ // The item is moved so name collisions are no longer an issue. // Unlock the user's root list. // // 3. UNLOCK OWNER'S ROOT LIST. self::releaseUserRootListLock( $ownerId ); // // Update ancestor sizes. // ---------------------- // Update parent ancestor sizes. $oldParent = self::load( $oldParentId ); if ( $oldParent !== NULL) { $oldParent ->updateSizeAndAncestors(); } // // Unlock original root's folder tree. // ----------------------------------- // Changes are done for the original folder tree so unlock. // // 2. UNLOCK ORIGINAL ROOT FOLDER TREE. self::releaseRootOperationLock( $originalRootId ); // // Hook & log. // ----------- // Note the change, even though descendants haven't been updated yet. $requestingUid = self::getCurrentUserId()[0]; self::postOperationHook( 'move' , [ $this , $oldParent , NULL, $requestingUid , ]); ManageLog::activity( "Moved @kind '@name' (# @id) to top level." , [ '@id' => $this ->id(), '@kind' => $this ->getKind(), '@name' => $this ->getName(), 'entity' => $this , 'uid' => $requestingUid , ]); if ( $oldParent !== NULL) { unset( $oldParent ); } // Garbage collect. gc_collect_cycles(); if ( $taskNeeded === FALSE) { // No scheduled task is needed. // // 1. UNLOCK THIS ITEM AS ROOT FOLDER TREE. self::releaseRootOperationLock( $this ->id()); return ; } // // Update descendants. // ------------------- // Finishing the move requires updating the root ID of all descendants. // // 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 moved item, which is now a root, and its folder tree. // // This will be unlocked by the task when the entire move is done. $parameters = [ 'updateIds' => [(int) $this ->id()], ]; $started = time(); $comments = 'Start move to root' ; $executionTime = 0; if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) { self::processTaskMoveToRoot( $requestingUid , $parameters , $started , $comments , $executionTime , TRUE); } else { FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskInitialDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskMoveToRoot' , $requestingUid , $parameters , $started , $comments , $executionTime ); } } /** * Moves multiple items to the root. * * Each item's current root folder tree, and the item's own folder tree, * are both locked at the start of the operation. This will prevent any * other edit operation from being performed on either folder tree. After * each item is moved to become a root item, the original root folder tree * is unlocked. The lock on the item's own folder tree remains until all * descendants have been updated and the move completes. * * Each item's parent and root IDs are updated to move it into the root list. * Each item is then given default access grants that give the user, and * only the user, access. * * If an item is a folder, a background task is scheduled to complete the * move by recursively traversing through the folder's descendants to set * each one's root ID. After all descendants have been updated, the root * folder tree is unlocked. Because the move executes as a background task, * completion of the move will occur after this method returns and at a * time in the future that depends upon the size of the folder tree being * moved and server load. * * System hidden and disabled items are also affected. * * <B>Background move</B> * File moves occur immediately, but folder moves schedule background * tasks to traverse the folder tree and update descendants. This will * delay completion of the move 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_move" hook after * each item is moved. The hook is not called for an item's descendants * since they have not moved and remain descendants of the original item. * * <B>Process locks</B> * This method locks each item's original root folder tree, and the item's * own folder tree for exclusive use during the move. This will prevent * any other edit operation from being performed on the same folder trees * until the move completes. The original root folder tree lock is released * as soon as each item has moved to the root list. When moves require * scheduled background tasks, unlocking each item's own root folder tree * does not occur until the last descendant is updated. * * <B>Activity log</B> * This method posts a log message after each item is moved. Log messages * are not posted as the item's descendants are updated since they have * not moved and remain descendants of the original item. * * @param int[] $ids * An array of integer FolderShare entity IDs to move. Invalid IDs * are silently skipped. * @param bool $allowRename * (optional, default = FALSE) When FALSE, each item retains its same * name as it is moved into the user's root list. If there is already an * item with the same name there, an exception is thrown. When TRUE, item * names may be adjusted to make them unique if there is an item with * the same name in the user's root list. * * @throws \Drupal\foldershare\Entity\Exception\LockException * Throws an exception if an access lock could not be acquired. * @throws \Drupal\foldershare\Entity\Exception\ValidationException * Throws an exception if a name is already in use in the user's root list. * * @see ::moveToRoot() */ public static function moveToRootMultiple( array $ids , bool $allowRename = FALSE) { // ------------------------------------------------------------------ // Each item can be: // - A file or folder. // - At the root level or in a subfolder. // - Owned by the current user or another. // // Typical use: // - All of the items are from a user interface selection. That selection // constrains them all to be children of the same parent folder. This // will put them all in the same root folder tree. // // Special cases: // - If the item is already at the root level, do nothing with it. // // - If the item is owned by another user, it is moved to the OWNER's // root list, not the current user's. It is up to the caller to have // decided if this is valid (admins can do this, while regular users // should not). // // Errors: // - The name is in use in the root list and renaming is not allowed. // // Actions: // - All items are sorted into groups with a shared root. In typical use, // there will be just one shared root. The root lists of all owners // are locked. // // - Check all names or create unique names. This is done with the OWNER's // root list for each item, not the current user's root list (though the // current user is often the only owner involved). // // - For all files and folders in the same root group: Lock the shared // root, set names, set parents and roots, set disabled (if a folder), // update old parent size (if any), and unlock shared root. // // - After all groups: Unlock all root lists and schedule a task if there // are any descendants to update. The task recurses through // the items setting root IDs, enables the item, then unlocks the item's // root folder tree. // // ------------------------------------------------------------------. if ( empty ( $ids ) === TRUE) { // Nothing to move. return ; } $requestingUid = self::getCurrentUserId()[0]; 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 ->moveToRoot( '' , $allowRename ); unset( $item ); return ; } // // Group IDs by root and validate. // ------------------------------- // The IDs given could be from scattered locations. Group them by // their current roots so that root folder locks can be done efficiently. // // Items in a root group may not all be owned by the same user. // // Along the way, skip anything that is already a root and collect a // list of owner IDs. Below we'll have to lock the root list for each // of these owners. $items = self::loadMultiple( $ids ); $reducedItems = []; $rootGroups = []; $ownerIds = []; foreach ( $items as $item ) { if ( $item === NULL) { // The item does not exist. continue ; } if ( $item ->isRootItem() === TRUE) { // The item is already a root. unset( $item ); continue ; } $reducedItems [] = $item ; $ownerIds [] = $item ->getOwnerId(); $rootId = $item ->getRootItemId(); $rootGroups [ $rootId ][] = $item ; } if ( empty ( $rootGroups ) === TRUE) { // Nothing to do. return ; } $ownerIds = array_unique ( $ownerIds ); $items = $reducedItems ; // // Lock owner root lists. // ---------------------- // For each owner, lock the root list. We need these locked while // we check for name collisions and optionally renaming items. // // 1. LOCK OWNER ROOT LISTS. foreach ( $ownerIds as $index => $ownerId ) { if (self::acquireUserRootListLock( $ownerId ) === FALSE) { // Failed to get lock. Back out any prior root list locks. // // 1. UNLOCK OWNER ROOT LISTS. foreach ( $ownerIds as $i => $o ) { if ( $i >= $index ) { break ; } self::releaseUserRootListLock( $o ); } throw new LockException( self::getStandardLockExceptionMessage(t( 'moved' ), NULL)); } } // // Check names. // ------------ // If renaming is not allowed, check if the name is already in use in // the owner's root list and abort if it is. // // If renaming is allowed, create a new unique name for each item, // checking the owner's root list to create each one. // // Note that we check the OWNER's root list, not the current user's // root list (which is often the same as the owner). A move to root of // an item not owned by the current user goes to the owner's root list, // so that's where we need to check for name uniqueness. // // Start by collecting the root names for each of the owners. $rootNamesByOwner = []; foreach ( $ownerIds as $ownerId ) { $rootNamesByOwner [ $ownerId ] = self::findAllRootItemNames( $ownerId ); } $itemNames = []; if ( $allowRename === FALSE) { // Check that names do not collide. foreach ( $items as $item ) { $ownerId = $item ->getOwnerId(); $rootNames = $rootNamesByOwner [ $ownerId ]; if (in_array( $item ->getName(), $rootNames ) === TRUE) { // 1. UNLOCK OWNER ROOT LISTS. foreach ( $ownerIds as $ownerId ) { self::releaseUserRootListLock( $ownerId ); } throw new ValidationException( self::getStandardRenameFirstExceptionMessage(NULL)); } // Add the item's name to the name list because it too is a collision // target for the next items. $rootNamesByOwner [ $ownerId ][ $item ->getName()] = (int) $item ->id(); $itemNames [ $item ->id()] = $item ->getName(); } } else { // Create non-colliding names. foreach ( $items as $item ) { $ownerId = $item ->getOwnerId(); $rootNames = $rootNamesByOwner [ $ownerId ]; $newName = self::createUniqueName( $rootNames , $item ->getName()); if ( $newName === FALSE) { // This is very very unlikely because creating a unique name tries // repeatedly to append a number until it gets to something unique. // // 1. UNLOCK OWNER ROOT LISTS. foreach ( $ownerIds as $ownerId ) { self::releaseUserRootListLock( $ownerId ); } throw new ValidationException( self::getStandardCannotCreateUniqueNameExceptionMessage( 'move' )); } // Add the item's name to the name list because it too is a collision // target for the next items. $rootNamesByOwner [ $ownerId ][ $newName ] = (int) $item ->id(); $itemNames [(int) $item ->id()] = $newName ; } } unset( $rootNamesByOwner ); // // Loop over root groups. // ---------------------- // Each root requires its own root folder tree lock as items are moved // out of that root folder and into the owner's root list. $updateIds = []; $nLockExceptions = 0; foreach ( $rootGroups as $originalRootId => $items ) { // // Lock these items' current root folder tree. // ------------------------------------------- // Lock the root folder tree these items are coming from so that the // folder tree cannot be changed during the move. // // 2. LOCK ORIGINAL ROOT'S FOLDER TREE. if (self::acquireRootOperationLock( $originalRootId ) === FALSE) { ++ $nLockExceptions ; $rootGroups [ $originalRootId ] = NULL; continue ; } // Clear the root group entry to save memory. $rootGroups [ $originalRootId ] = NULL; // // Update parent and root IDs. // --------------------------- // Update these items, clearing the parent and root IDs (since the item // is becoming a root). Clear the access grants and set them to defaults // for a root. Update the name, if needed. foreach ( $items as $index => $item ) { // This item is becoming a root. If it is a folder, we'll have more // work to do to update descendants, so lock it. // // 3. LOCK THIS ITEM AS ROOT FOLDER TREE, if it is a folder. if ( $item ->isFolder() === TRUE) { if (self::acquireRootOperationLock( $item ->id()) === FALSE) { ++ $nLockExceptions ; unset( $item ); $items [ $index ] = NULL; continue ; } } // // Decide if a task will be needed. // -------------------------------- // If the item being moved is: // - A folder. // - With children. // // Then we'll need to schedule a task. $taskNeeded = ( $item ->isFolder() === TRUE && $item ->findNumberOfChildren() > 0); $items [ $index ] = NULL; $oldParentId = $item ->getParentFolderId(); $item ->clearParentFolderId(); $item ->clearRootItemId(); $item ->clearAccessGrants(); $item ->setSystemDisabled( $taskNeeded ); $item ->setName( $itemNames [(int) $item ->id()]); $item ->save(); // If the item is a file, image, or media object change the underlying // item's name too. $item ->renameWrappedFile( $itemNames [(int) $item ->id()]); // // Update ancestor sizes. // ---------------------- // If the item was not a root, it has a parent. Update the parent's // ancestor sizes now that the item has moved. $oldParent = self::load( $oldParentId ); if ( $oldParent !== NULL) { $oldParent ->updateSizeAndAncestors(); } // // Hook & log. // ----------- // Note the change, even though descendants haven't been updated yet. self::postOperationHook( 'move' , [ $item , $oldParent , NULL, $requestingUid , ]); ManageLog::activity( "Moved @kind '@name' (# @id) to top level." , [ '@id' => $item ->id(), '@kind' => $item ->getKind(), '@name' => $item ->getName(), 'entity' => $item , 'uid' => $requestingUid , ]); if ( $taskNeeded === TRUE) { // A scheduled task is needed to update descendant root IDs. $updateIds [] = (int) $item ->id(); } if ( $oldParent !== NULL) { unset( $oldParent ); } unset( $item ); // Garbage collect. gc_collect_cycles(); } unset( $items ); // // Unlock original root's folder tree. // ----------------------------------- // Modifications to the old one are now done. Everything has been // moved out of it. Unlock it. // // 2. UNLOCK ORIGINAL ROOT'S FOLDER TREE. self::releaseRootOperationLock( $originalRootId ); } unset( $rootGroups ); // Unlock owner's root lists. // ------------------------- // Everything has been moved to the owner's root list and, possibly, // given new names. We no longer need to keep the root lists locked. // // 1. UNLOCK OWNER ROOT LISTS. foreach ( $ownerIds as $ownerId ) { self::releaseUserRootListLock( $ownerId ); } if ( empty ( $updateIds ) === TRUE) { // No descendants to process. Done. if ( $nLockExceptions !== 0) { throw new LockException( self::getStandardLockExceptionMessage(t( 'moved' ), NULL)); } return ; } // // Update descendants. // ------------------- // Finishing the move requires updating the root ID of all descendants. // // 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: // // - Every item being updated is a root with a root folder tree lock. // // This will be unlocked by the task when the entire move is done. $parameters = [ 'updateIds' => $updateIds , ]; $started = time(); $comments = 'Start move to root' ; $executionTime = 0; if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) { self::processTaskMoveToRoot( $requestingUid , $parameters , $started , $comments , $executionTime , TRUE); } else { FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskInitialDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskMoveToRoot' , $requestingUid , $parameters , $started , $comments , $executionTime ); } if ( $nLockExceptions !== 0) { throw new LockException( self::getStandardLockExceptionMessage(t( 'moved' ), NULL)); } } /*--------------------------------------------------------------------- * * Move to folder. * *---------------------------------------------------------------------*/ /** * {@inheritdoc} */ public function moveToFolder( FolderShareInterface $destination = NULL, string $newName = '' , bool $allowRename = FALSE) { // ------------------------------------------------------------------ // 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 no destination is given, assume a move to the root and redirect // to moveToRoot(). // // - If the item is owned by another user, it remaines owned by them. // It is up to the caller to insure the current user has permission // to move the item. // // - If the item is not a descendant of the destination, then the item's // ancestor root needs to be locked along with the destination's. // // - If the item is a descendant of the destination, then locking just // the destination's root is sufficient. // // - If the item is at the root level, then no root list lock is needed // because we don't need to check for root list collisions on a move // out of the root list. We only need to check on moves/copies/adds // in to the root list. // // Errors: // - The destination is not a folder. // - The destination is a descendant of this item (a circular move). // - The new name is illegal. // - The new name is in use in the destination folder and renaming is // not allowed. // // Actions: // - If the item is a file: Lock destination's root, lock item's root (if // different), set name, set parent and root, update old parent // size (if any), unlock old item's root (if not same as destination), // unlock destination's root. // // - If the item is a folder: Lock destination's root, lock item's root (if // different), set name, clear parent and root, set access grants, // set disabled, update old parent size, unlock old item's root (if not // same as destination), and schedule task. The task recurses through // the item setting root IDs then unlocks the destination's root. // // ------------------------------------------------------------------ // // Validate. // --------- // Confirm that the destination is a folder and that it is not a // descendant of this item. if ( $destination === NULL) { return $this ->moveToRoot( $newName , $allowRename ); } if ( $destination ->isFolder() === FALSE) { throw new ValidationException(FormatUtilities::createFormattedMessage( t( '@method was called with a move destination that is not a folder.' , [ '@method' => __METHOD__ , ]))); } $destinationId = (int) $destination ->id(); if ( $this ->isRootItem() === FALSE) { // If the destination is this item's parent, then this is really // a rename, which is handled separately. if ( $destinationId === $this ->getParentFolderId()) { if ( empty ( $newName ) === TRUE) { // The item is already in the destination. return ; } $this ->rename( $newName ); return ; } } // If the destination is a descendant of this item, then the move is // circular. if ( $destinationId === (int) $this ->id() || $this ->isAncestorOfFolderId( $destinationId ) === TRUE) { throw new ValidationException(FormatUtilities::createFormattedMessage( t( 'The item "@name" cannot be moved into one of its own descendants.' , [ '@name' => $this ->getName(), ]))); } $requestingUid = self::getCurrentUserId()[0]; // // Check name legality. // -------------------- // If there is a new name, make sure it is legal. if ( empty ( $newName ) === FALSE) { // The checkName() function throws an exception if the name is too // long or uses illegal characters. $this ->checkName( $newName ); } else { $newName = $this ->getName(); } // // Lock the destination root's folder tree. // ---------------------------------------- // The destination is about to be modified by the addition of this item. // Get a lock on that root folder so that it cannot change out from under // this move. // // Since this lock will be the last one to be released, it is important // that it be the first to be acquired so that we don't get race conditions. // // 1. LOCK DESTINATION ROOT FOLDER TREE. $destinationRootId = $destination ->getRootItemId(); if (self::acquireRootOperationLock( $destinationRootId ) === FALSE) { throw new LockException( self::getStandardLockExceptionMessage(t( 'moved' ), $this ->getName())); } // // Lock the item root's folder tree, if different from destination's. // ------------------------------------------------------------------ // If this item is a root item, then this will lock the item itself. // // If this item is in the same root folder tree as the destination, // then that root folder tree is already locked. Do nothing more. // // Otherwise this item is in some other root folder tree. Lock it. $originalRootId = $this ->getRootItemId(); if ( $originalRootId !== $destinationRootId ) { // 2. LOCK ORIGINAL ROOT'S FOLDER TREE. if (self::acquireRootOperationLock( $originalRootId ) === FALSE) { // 1. UNLOCK DESTINATION ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationRootId ); throw new LockException( self::getStandardLockExceptionMessage(t( 'moved' ), $this ->getName())); } } // // Check name. // ----------- // If renaming is not allowed, check if the name is already in use in // the destination folder and abort if it is. // // If renaming is allowed, create a new unique name in the destination // folder. if ( $allowRename === FALSE) { if (self::findNamedChildId( $destinationId , $newName ) !== FALSE) { if ( $originalRootId !== $destinationRootId ) { // 2. UNLOCK ORIGINAL ROOT'S FOLDER TREE. self::releaseRootOperationLock( $originalRootId ); } // 1. UNLOCK DESTINATION ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationRootId ); throw new ValidationException( self::getStandardRenameFirstExceptionMessage( $newName )); } } else { $siblingNames = $destination ->findChildrenNames(); $newName = self::createUniqueName( $siblingNames , $newName ); if ( $newName === FALSE) { // This is very very unlikely because creating a unique name tries // repeatedly to append a number until it gets to something unique. throw new ValidationException( self::getStandardCannotCreateUniqueNameExceptionMessage( 'move' )); } unset( $siblingNames ); } // // Decide if a task will be needed. // -------------------------------- // If the item being moved is: // - A folder. // - With children. // - And the root folder has changed. // // Then we'll need to schedule a task. $taskNeeded = ( $this ->isFolder() === TRUE && $originalRootId !== $destinationRootId && $this ->findNumberOfChildren() > 0); // // Update parent and root IDs. // --------------------------- // Update this item, swapping in the destination ID as the new parent, // and the new root ID (which might not be different). // // Since the item is now in a subfolder, clear all access grants. // // If the item is a folder and it was not in the same root folder tree // as the destination (e.g. it was a root or in another folder tree), // then we'll have to update descendants below. Mark the item disabled // until that is done. $oldParentId = $this ->getParentFolderId(); $this ->setParentFolderId( $destinationId ); $this ->setRootItemId( $destinationRootId ); $this ->clearAccessGrants(self::ANY_USER_ID, FALSE); $this ->setSystemDisabled( $taskNeeded ); $this ->setName( $newName ); $this ->save(); // If the item is a file, image, or media object change the underlying // item's name too. $this ->renameWrappedFile( $newName ); // // Update ancestor sizes. // ---------------------- // Update destination ancestor sizes to include the addition of the // moved item. Since the moved item already has a size field set, // the update can be correct even though we haven't finished updating // descendants. // // If the item was not a root, then it had an old parent folder. // Update ancestor sizes for that old parent to reflect the loss of // the moved item. $destination ->updateSizeAndAncestors(); if ( $oldParentId < 0) { // No parent. Item was a root. $oldParent = NULL; } else { $oldParent = self::load( $oldParentId ); if ( $oldParent !== NULL) { $oldParent ->updateSizeAndAncestors(); } } // // Unlock original root's folder tree, if different from now. // ---------------------------------------------------------- // If the item moved within the same root folder tree as the destination, // then do nothing. No additional root folder tree lock was needed. // // Otherwise, the item was a root or it was in some other root folder tree. // Unlock that tree since we are now done with it. if ( $originalRootId !== $destinationRootId ) { // 2. UNLOCK ORIGINAL ROOT'S FOLDER TREE. self::releaseRootOperationLock( $originalRootId ); } // // Hook & log. // ----------- // Note the change, even though descendants haven't been updated yet. self::postOperationHook( 'move' , [ $this , $oldParent , $destination , $requestingUid , ]); ManageLog::activity( "Moved @kind '@name' (# @id) to '@destName' (# @destId)." , [ '@id' => $this ->id(), '@kind' => $this ->getKind(), '@name' => $this ->getName(), '@destId' => $destination ->id(), '@destName' => $destination ->getName(), 'entity' => $this , 'uid' => $requestingUid , ]); if ( $oldParent !== NULL) { unset( $oldParent ); } unset( $destination ); if ( $taskNeeded === FALSE) { // No scheduled task is needed. // // 1. UNLOCK DESTINATION ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationRootId ); return ; } // // Update descendants. // ------------------- // Finishing the move requires updating the root ID of all descendants. // // 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 destination folder's root folder tree. // // This will be unlocked by the task when the entire move is done. $parameters = [ 'updateIds' => [(int) $this ->id()], 'unlockRootId' => $destinationRootId , ]; $started = time(); $comments = 'Start move to folder' ; $executionTime = 0; if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) { self::processTaskMoveToFolder( $requestingUid , $parameters , $started , $comments , $executionTime , TRUE); } else { FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskInitialDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskMoveToFolder' , $requestingUid , $parameters , $started , $comments , $executionTime ); } } /** * Moves multiple items to a folder. * * Each item's current root folder tree, and the destination root folder tree, * are both locked at the start of the operation. This will prevent any * other edit operation from being performed on either folder tree. After * each item is moved, the original root folder tree is unlocked. The lock * on the destination root folder tree remains until all descendants have * been updated and the move completes. * * Each item's parent and root IDs are updated to move it into the * destination. Access grants are cleared. * * If an item is a folder, a background task is scheduled to complete the * move by recursively traversing through the folder's descendants to set * each one's root ID. After all descendants have been updated, the * destination root folder tree is unlocked. Because the move executes as * a background task, completion of the move will occur after this method * returns and at a time in the future that depends upon the size of the * folder tree being moved and server load. * * System hidden and disabled items are also affected. * * <B>Background move</B> * File moves occur immediately, but folder moves schedule background * tasks to traverse the folder tree and update descendants. This will * delay completion of the move 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_move" hook after * each item is moved. The hook is not called for an item's descendants * since they have not moved and remain descendants of the original item. * * <B>Process locks</B> * This method locks each item's original root folder tree, and the * destination root folder tree for exclusive use during the move. This * will prevent any other edit operation from being performed on the same * folder trees until the move completes. The original root folder tree * lock is released as soon as each item has moved to the destination. * When moves require scheduled background tasks, unlocking the destination * root folder tree does not occur until the last descendant is updated. * * <B>Activity log</B> * This method posts a log message after each item is moved. Log messages * are not posted as the item's descendants are updated since they have * not moved and remain descendants of the original item. * * @param int[] $ids * An array of integer FolderShare entity IDs to move. Invalid IDs * are silently skipped. * @param \Drupal\foldershare\FolderShareInterface $destination * (optional, default = NULL = move to the root list) The destination * folder for the move. When NULL, the moved items are added to the * user's root list. * @param bool $allowRename * (optional, default = FALSE) When FALSE, each item retains its same * name as it is moved into the destination. If there is already an item * with the same name there, an exception is thrown. When TRUE, item * names may be adjusted to make them unique if there is an item with * the same name in the destination. * * @throws \Drupal\foldershare\Entity\Exception\LockException * Throws an exception if an access lock could not be acquired. * @throws \Drupal\foldershare\Entity\Exception\ValidationException * Throws an exception if a name is already in use in the destination. * * @see ::moveToFolder() */ public static function moveToFolderMultiple( array $ids , FolderShareInterface $destination = NULL, bool $allowRename = FALSE) { // ------------------------------------------------------------------ // Each item can be: // - A file or folder. // - At the root level or in a subfolder. // - Owned by the current user or another. // // Typical use: // - All of the items are from a user interface selection. That selection // constrains them all to be roots or all children of the same parent // folder. // // Special cases: // - If the item is already in the destination, do nothing with it. // // - If the item is owned by another user, it is moved to the OWNER's // root list, not the current user's. It is up to the caller to have // decided if this is valid (admins can do this, while regular users // should not). // // Errors: // - The destination is not a folder. // - The destination is a descendant of an item (a circular move). // - An item's name is in use in the destination folder and renaming is // not allowed. // // Actions: // - All items are sorted into groups with a shared root. In typical use, // there will be just one shared root. The destination is locked. // // - Check all names or create unique names. // // - For all files and folders in the same root group: Lock the shared // root (if different from destination root), set names, set parents // and roots, set disabled (if a folder), update old parent size // (if any), and unlock shared root (if different from destination root). // // - After all groups: Update destination size and schedule a task if there // are any descendants to update. The task recurses through the items // setting root IDs, enables the item, then unlocks the destination root. // // ------------------------------------------------------------------. if ( empty ( $ids ) === TRUE) { // Nothing to move. return ; } if ( $destination === NULL) { // If there is no destination, move to root. self::moveToRootMultiple( $ids , $allowRename ); 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 ->moveToFolder( $destination , '' , $allowRename ); unset( $item ); return ; } $requestingUid = self::getCurrentUserId()[0]; // // Validate. // --------- // The destination must be a folder. if ( $destination ->isFolder() === FALSE) { throw new ValidationException(FormatUtilities::createFormattedMessage( t( '@method was called with a move destination that is not a folder.' , [ '@method' => __METHOD__ , ]))); } // // Group IDs by root and validate. // ------------------------------- // The IDs given could be from scattered locations. Group them by // their current roots so that root folder locks can be done efficiently. // // Along the way, check for circular moves. $items = self::loadMultiple( $ids ); $reducedItems = []; $rootGroups = []; $destinationId = (int) $destination ->id(); foreach ( $items as $item ) { if ( $item === NULL) { // The item does not exist. continue ; } if ( $item ->getParentFolderId() === $destinationId ) { // The item is already in the destination. unset( $item ); continue ; } // If the destination is a descendant of this item, then the move is // circular. if ( $destinationId === (int) $item ->id() || $item ->isAncestorOfFolderId( $destinationId ) === TRUE) { throw new ValidationException(FormatUtilities::createFormattedMessage( t( 'The item "@name" cannot be moved into one of its own descendants.' , [ '@name' => $item ->getName(), ]))); } $rootId = $item ->getRootItemId(); $rootGroups [ $rootId ][] = $item ; $reducedItems [] = $item ; } if ( empty ( $rootGroups ) === TRUE) { // Nothing to move. return ; } $items = $reducedItems ; // // Lock the destination root's folder tree. // ---------------------------------------- // The destination is about to be modified by the addition of these items. // Get a lock on that root folder so that it cannot change out from under // this move. // // 1. LOCK DESTINATION ROOT FOLDER TREE. $destinationRootId = $destination ->getRootItemId(); if (self::acquireRootOperationLock( $destinationRootId ) === FALSE) { throw new LockException( self::getStandardLockExceptionMessage(t( 'moved' ), NULL)); } // // Check names. // ------------ // If renaming is not allowed, check if the name is already in use in // the destination and abort if it is. // // If renaming is allowed, create a new unique name for each item, // checking the destination to create each one. $itemNames = []; $siblingNames = $destination ->findChildrenNames(); if ( $allowRename === FALSE) { foreach ( $items as $item ) { if (in_array( $item ->getName(), $siblingNames ) === TRUE) { // 1. UNLOCK DESTINATION ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationRootId ); throw new ValidationException( self::getStandardRenameFirstExceptionMessage( $item ->getName())); } // Add the new name to the list of sibling names since it is now // taken and cannot be reused by the next item. $siblingNames [ $item ->getName()] = (int) $item ->id(); $itemNames [(int) $item ->id()] = $item ->getName(); } } else { // Create non-colliding names. foreach ( $items as $item ) { $newName = self::createUniqueName( $siblingNames , $item ->getName()); if ( $newName === FALSE) { // This is very very unlikely because creating a unique name tries // repeatedly to append a number until it gets to something unique. // // 1. UNLOCK DESTINATION ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationRootId ); throw new ValidationException( self::getStandardCannotCreateUniqueNameExceptionMessage( 'move' )); } // Add the new name to the list of sibling names since it is now // taken and cannot be reused by the next item. $siblingNames [ $newName ] = (int) $item ->id(); $itemNames [(int) $item ->id()] = $newName ; } } unset( $siblingNames ); // // Loop over root groups. // ---------------------- // Each root requires its own root folder tree lock as the item is moved // out of that root folder and into the destination root's folder tree. $nLockExceptions = 0; $updateIds = []; foreach ( $rootGroups as $originalRootId => $items ) { // // Lock these item's current root folder tree, if changing. // -------------------------------------------------------- // If these items are moving from one root folder tree to another, lock // the root folder tree they are coming from so that it cannot be changed // during the move. if ( $originalRootId !== $destinationRootId ) { // 2. LOCK ORIGINAL ROOT'S FOLDER TREE. if (self::acquireRootOperationLock( $originalRootId ) === FALSE) { ++ $nLockExceptions ; $rootGroups [ $originalRootId ] = NULL; continue ; } } // Unset the root group entry to save memory. $rootGroups [ $originalRootId ] = NULL; // // Update parent and root IDs. // --------------------------- // Update these items, swapping in the destination ID as the new parent, // and the new root ID (which might not be different). Since the items // are definitely not roots now (though they may have been before), clear // the access grants because they are no longer relevant. // // Disable items that are folders that are not moving within the same // root folder tree. Such items have descendants that need their root IDs // updated, so keep the folder disabled until that update is done. foreach ( $items as $item ) { if ( $item === NULL) { continue ; } // // Decide if a task will be needed. // -------------------------------- // If the item being moved is: // - A folder. // - With children. // - And the root folder has changed. // // Then we'll need to schedule a task. $taskNeeded = ( $item ->isFolder() === TRUE && $originalRootId !== $destinationRootId && $item ->findNumberOfChildren() > 0); $oldParentId = $item ->getParentFolderId(); $item ->setParentFolderId( $destinationId ); $item ->setRootItemId( $destinationRootId ); $item ->clearAccessGrants(self::ANY_USER_ID, FALSE); $item ->setSystemDisabled( $taskNeeded ); $item ->setName( $itemNames [(int) $item ->id()]); $item ->save(); // If the item is a file, image, or media object change the underlying // item's name too. $item ->renameWrappedFile( $itemNames [(int) $item ->id()]); // // Update ancestor sizes. // ---------------------- // Update parent (if any) ancestor sizes. if ( $oldParentId < 0) { $oldParent = NULL; } else { $oldParent = self::load( $oldParentId ); if ( $oldParent !== NULL) { $oldParent ->updateSizeAndAncestors(); } } if ( $taskNeeded === TRUE) { // The item is a folder and it is moving from one root folder tree // to another (and thus the folder's descendants need their // root IDs updated). Add the folder's ID to the to-be-updated list. $updateIds [] = (int) $item ->id(); } // // Hook & log. // ----------- // Note the change, even though descendants haven't been updated yet. self::postOperationHook( 'move' , [ $item , $oldParent , $destination , $requestingUid , ]); ManageLog::activity( "Moved @kind '@name' (# @id) to '@destName' (# @destId)." , [ '@id' => $item ->id(), '@kind' => $item ->getKind(), '@name' => $item ->getName(), '@destId' => $destination ->id(), '@destName' => $destination ->getName(), 'entity' => $item , 'uid' => $requestingUid , ]); if ( $oldParent !== NULL) { unset( $oldParent ); } } unset( $items ); // Garbage collect. gc_collect_cycles(); // // Unlock original root's folder tree, if different from now. // ---------------------------------------------------------- // If the item has moved from one root folder tree to another, // modifications to the old one are now done. Unlock it. if ( $originalRootId !== $destinationRootId ) { // 2. UNLOCK ORIGINAL ROOT'S FOLDER TREE. self::releaseRootOperationLock( $originalRootId ); } } unset( $rootGroups ); // // Update ancestor sizes. // ---------------------- // Update destination ancestor sizes. $destination ->updateSizeAndAncestors(); if ( empty ( $updateIds ) === TRUE) { // No descendants to process. Done. // // 1. UNLOCK DESTINATION ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationRootId ); if ( $nLockExceptions !== 0) { throw new LockException( self::getStandardLockExceptionMessage(t( 'moved' ), NULL)); } return ; } unset( $destination ); // // Update descendants. // ------------------- // Finishing the move requires updating the root ID of all descendants. // // 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 destination folder's root folder tree. // // This will be unlocked by the task when the entire move is done. $parameters = [ 'updateIds' => $updateIds , 'unlockRootId' => $destinationRootId , ]; $started = time(); $comments = 'Start move to folder' ; $executionTime = 0; if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) { self::processTaskMoveToFolder( $requestingUid , $parameters , $started , $comments , $executionTime , TRUE); } else { FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskInitialDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskMoveToFolder' , $requestingUid , $parameters , $started , $comments , $executionTime ); } if ( $nLockExceptions !== 0) { throw new LockException( self::getStandardLockExceptionMessage(t( 'moved' ), NULL)); } } /*--------------------------------------------------------------------- * * Background task handling. * *---------------------------------------------------------------------*/ /** * Processes a scheduled move-to-root task to update move 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 move task is provided a list of root IDs for moved entities. For * each one, the task recurses through folder entities, updating the root * ID of children before updating the folder itself. * * <B>Process locks</B> * This method releases the root folder tree lock acquired for each item * when the task was started. * * <B>Post-operation hooks</B> * No hooks are called. * * <B>Activity log</B> * No log messages are posted. * * @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: * - 'updateIds': the IDs of entities to recurse downwards from and * set their root IDs. All of them are roots and are unlocked after * the update is done. * @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 ::setRootItemId() * @see ::moveToFolder() * @see ::moveToFolderMultiple() * @see ::moveToRoot() * @see ::moveToRootMultiple() */ public static function processTaskMoveToRoot( 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 [ 'updateIds' ]) === FALSE || is_array ( $parameters [ 'updateIds' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , 'updateIds' ); return ; } $updateIds = $parameters [ 'updateIds' ]; // // 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::processTaskMoveToRoot' , $requestingUid , [ 'updateIds' => $updateIds , ], $started , 'Safety-net requeue' , $executionTime ); // // Prepare. // -------- // Garbage collect and initialize. $beginTime = time(); $opCounter = 0; gc_collect_cycles(); // // Update descendants with new root ID. // ------------------------------------ // Loop over all descendants that DO NOT have the correct root ID // already and update their root IDs. // // 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 already have the right // root ID, a repeated run will find fewer things to change and go quicker. // Over repeated runs, this will eventually complete. foreach ( $updateIds as $updateIndex => $id ) { $item = self::load( $id ); if ( $item === NULL) { // Item does not exist. continue ; } // Increment to count load. ++ $opCounter ; // In a move to root, the queued item is now a root and all of its // descendants need to use it as their root. $rootId = (int) $item ->id(); // Find all descendants that do NOT have the correct root ID. $descendantIds = $item ->findDescendantIdsByRootId( $rootId , FALSE); // Loop over these. Load each one and change it. foreach ( $descendantIds as $descendantId ) { $descendant = self::load( $descendantId ); if ( $descendant === NULL) { // Descendant does not exist. continue ; } // Increment to count load. ++ $opCounter ; // Set and save. $descendant ->setRootItemId( $rootId ); $descendant ->save(); // Increment to count save. ++ $opCounter ; unset( $descendant ); // Check memory and execution time usage every so often. if ( $opCounter >= self::USAGE_CHECK_INTERVAL) { $reschedule = FALSE; if (( $interactive === TRUE && LimitUtilities::aboveResponseExecutionTimeLimit() === TRUE) || LimitUtilities::aboveExecutionTimeLimit() === TRUE) { $reschedule = TRUE; $reason = 'time limit' ; } elseif (LimitUtilities::aboveMemoryUseLimit() === TRUE) { $reschedule = TRUE; $reason = 'memory use limit' ; } if ( $reschedule === TRUE) { // 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 task's root lock since we aren't done yet. // // Schedule continuation task. Execution has already unset // entries in the $updateIds list as they were finished. FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskContinuationDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskMoveToRoot' , $requestingUid , [ 'updateIds' => $updateIds , ], $started , "Continuation due to $reason after $opCounter ops" , $executionTime + (time() - $beginTime )); // Delete the safety net task. FolderShareScheduledTask::deleteTask( $safetyNetTask ); return ; } $opCounter = 0; } } $item ->setSystemDisabled(FALSE); $item ->save(); // Increment to count save. ++ $opCounter ; unset( $item ); unset( $descendantIds ); unset( $updateIds [ $updateIndex ]); // Garbage collect. gc_collect_cycles(); // UNLOCK ITEM ROOT FOLDER TREE. self::releaseRootOperationLock( $id ); } unset( $updateIds ); // Delete the safety net task. FolderShareScheduledTask::deleteTask( $safetyNetTask ); } /** * Processes a scheduled move-to-folder task to update move 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 move task is provided a list of IDs for moved entities. For each one, * the task recurses through folder entities, updating the root ID of * children before updating the folder itself. * * <B>Process locks</B> * This method releases the root folder tree lock acquired when the task * was started. * * <B>Post-operation hooks</B> * No hooks are called. * * <B>Activity log</B> * No log messages are posted. * * @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: * - 'updateIds': the IDs of entities to recurse downwards from and * set their root IDs. * - 'unlockRootId': the ID of the root to unlock upon completion. This * is also the root ID to set entities to use. * @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 ::setRootItemId() * @see ::moveToFolder() * @see ::moveToFolderMultiple() * @see ::moveToRoot() * @see ::moveToRootMultiple() */ public static function processTaskMoveToFolder( 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 a new // root ID. if (isset( $parameters [ 'updateIds' ]) === FALSE || is_array ( $parameters [ 'updateIds' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , 'updateIds' ); return ; } if (isset( $parameters [ 'unlockRootId' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , 'unlockRootId' ); return ; } $updateIds = $parameters [ 'updateIds' ]; $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::processTaskMoveToFolder' , $requestingUid , [ 'updateIds' => $updateIds , 'unlockRootId' => $unlockRootId , ], $started , 'Safety-net requeue' , $executionTime ); // // Prepare. // -------- // Garbage collect and initialize. $beginTime = time(); $opCounter = 0; gc_collect_cycles(); // // Update descendants with new root ID. // ------------------------------------ // Loop over all descendants that DO NOT have the correct root ID // already and update their root IDs. // // 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 already have the right // root ID, a repeated run will find fewer things to change and go quicker. // Over repeated runs, this will eventually complete. foreach ( $updateIds as $updateIndex => $id ) { $item = self::load( $id ); if ( $item === NULL) { // Item does not exist. continue ; } // Increment to count load. ++ $opCounter ; // Find all descendants that do NOT have the correct root ID. $descendantIds = $item ->findDescendantIdsByRootId( $unlockRootId , FALSE); // Loop over these. Load each one and change it. foreach ( $descendantIds as $descendantId ) { $descendant = self::load( $descendantId ); if ( $descendant === NULL) { // Descendant does not exist. continue ; } // Increment to count load. ++ $opCounter ; // Set and save. $descendant ->setRootItemId( $unlockRootId ); $descendant ->save(); // Increment to count save. ++ $opCounter ; unset( $descendant ); // Check memory and execution time usage every so often. if ( $opCounter >= self::USAGE_CHECK_INTERVAL) { $reschedule = FALSE; if (( $interactive === TRUE && LimitUtilities::aboveResponseExecutionTimeLimit() === TRUE) || LimitUtilities::aboveExecutionTimeLimit() === TRUE) { $reschedule = TRUE; $reason = 'time limit' ; } elseif (LimitUtilities::aboveMemoryUseLimit() === TRUE) { $reschedule = TRUE; $reason = 'memory use limit' ; } if ( $reschedule === TRUE) { // 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 task's root lock since we aren't done yet. // // Schedule continuation task. Execution has already unset // entries in the $updateIds list as they were finished. FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskContinuationDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskMoveToFolder' , $requestingUid , [ 'updateIds' => $updateIds , 'unlockRootId' => $unlockRootId , ], $started , "Continuation due to $reason after $opCounter ops" , $executionTime + (time() - $beginTime )); // Delete the safety net task. FolderShareScheduledTask::deleteTask( $safetyNetTask ); return ; } $opCounter = 0; } } $item ->setSystemDisabled(FALSE); $item ->save(); // Increment to count save. ++ $opCounter ; unset( $item ); unset( $descendantIds ); unset( $updateIds [ $updateIndex ]); // Garbage collect. gc_collect_cycles(); } unset( $updateIds ); // UNLOCK ROOT FOLDER TREE. self::releaseRootOperationLock( $unlockRootId ); // Delete the safety net task. FolderShareScheduledTask::deleteTask( $safetyNetTask ); } } |