foldershare-8.x-1.2/src/Entity/FolderShareTraits/OperationCopyTrait.php
src/Entity/FolderShareTraits/OperationCopyTrait.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\ExecutionTimeLimitException; use Drupal\foldershare\Entity\Exception\MemoryLimitException; use Drupal\foldershare\Entity\Exception\ValidationException; use Drupal\foldershare\Entity\Exception\SystemException; /** * Copy FolderShare entities. * * This trait includes methods to copy 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. * * <B>Memory use</B> * Copy operations necessarily require loading entities in order to copy * them. Larger folders and deeper folder trees have more entities and * require more entity loads. Each loaded entity uses memory. * * To keep memory use under control, these methods: * * - Flush loaded entities from the FolderShare entity memory cache * when they are no longer needed. * * - Execute the PHP garbage collector after entities are released. * * <B>Execution time</B> * Copy operations are expensive. They require loading, duplicating, and * saving entities, which causes a large number of database operations, * internal processing, and hook calls. When entities reference files, * those files must be copied on storage devices. Larger folders, larger * files, and deeper folder trees have more entities and more files to * copy, and all of this takes time. * * To keep execution time under control, these methods copy only the * first item immediately, then queue the rest as a scheduled task run * by future processes. * * @ingroup foldershare */ trait OperationCopyTrait { /*--------------------------------------------------------------------- * * Copy to root. * *---------------------------------------------------------------------*/ /** * {@inheritdoc} */ public function copyToRoot( 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, it can still be copied // if (1) the original is owned by another user, and thus the copy // will go to the current user's root list, or (2) the original is // owned by this user and $allowRename is TRUE so that a new copy can // be made that doesn't collide with itself. // // 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 source root folder tree, lock user // root list, check name collisions or rename, duplicate, unlock user // root list, and unlock source root folder tree. // // - If the item is a folder: Lock source root folder tree, lock user // root list, check name collisions or rename, duplicate, mark // disabled, unlock user root list, and schedule task. The task // recurses to copy the rest of the source into the copy, enables // the copy, unlocks the copy, and unlocks the source. // // ------------------------------------------------------------------ // // 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(); } // // Check for in-place copies. // -------------------------- // If the item is already a root, it is owned by the current user, // and $allowRename is FALSE, then it doesn't make sense. We cannot // copy an item already in the user's root without renaming it so it // won't collide with itself. $currentUserId = self::getCurrentUserId()[0]; if ( $this ->isRootItem() === TRUE && $this ->getOwnerId() === $currentUserId && $allowRename === FALSE) { // Issuing an error message that this combination does nothing // is not very informative. Just do nothing. return $this ; } // // Lock source root folder tree. // ----------------------------- // Lock the source root folder tree containing this item. If this item // is a root item, this locks itself. // // 1. LOCK SOURCE ROOT FOLDER TREE. $sourceRootId = $this ->getRootItemId(); if (self::acquireRootOperationLock( $sourceRootId ) === FALSE) { throw new LockException( self::getStandardLockExceptionMessage(t( 'copied' ), $this ->getName())); } // // Lock the user's root list. // -------------------------- // Lock the user's root list while we check for a name collision before // copying this item to the root list. No matter who owns the original, // it is getting copied to the current user's root list. // // 2. LOCK USER'S ROOT LIST. if (self::acquireUserRootListLock() === FALSE) { // 1. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); throw new LockException( self::getStandardLockExceptionMessage(t( 'copied' ), $this ->getName())); } // // Check name. // ----------- // Either validate that the name doesn't collide, or modify the name // so that it doesn't collide. // // Since the copy goes into the current user's root list, check // names there. if ( $allowRename === FALSE) { if ( empty (self::findAllRootItemIds( $currentUserId , $newName )) === FALSE) { // 2. UNLOCK USER'S ROOT LIST. self::releaseUserRootListLock(); // 1. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); throw new ValidationException( self::getStandardRenameFirstExceptionMessage( $newName )); } } else { $rootNames = self::findAllRootItemNames( $currentUserId ); $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( 'copy' )); } } // // Decide if a task will be needed. // -------------------------------- // If the item being copied is: // - A folder. // - With children. // // Then we'll need to schedule a task. $taskNeeded = ( $this ->isFolder() === TRUE && $this ->findNumberOfChildren() > 0); // // Duplicate WITHOUT recursion. // ---------------------------- // Duplicate the item alone, without recursing to duplicate its // possible descendants (yet). This copies the name and fields from // the source with these changes: // // - The current user is the new owner. // - The parent is the user's root list (i.e., no parent). // - The root is the user's root list (i.e., no root). // - The original name, a new name, or a new unique name. // - The new item is enabled if it is not a folder with children. try { $copy = $this ->duplicateInternal( $currentUserId , self::USER_ROOT_LIST, self::USER_ROOT_LIST, $newName , ! $taskNeeded ); } catch (\Exception $e ) { // On any exception, it is not safe to continue. // // One type of exception is a system exception, which indicates a // catastrophic file system problem that has already been logged. // // 2. UNLOCK USER'S ROOT LIST. self::releaseUserRootListLock(); // 1. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); throw $e ; } // // Unlock user's root list. // ------------------------ // The duplicate has been created into the user's root list with a // safe name. The root list lock is no longer needed. // // 2. UNLOCK USER'S ROOT LIST. self::releaseUserRootListLock(); // // Unlock the source root folder tree, if we're done. // Lock the new copy, if we're not done. // --------------------------------------------------- // If there are no descendants to copy, unlock the source root folder tree. // We are done with it. // // BUT if the item is a folder, keep the original root folder tree // locked because there's more to copy. Add a lock on the copy's root // folder tree. if ( $taskNeeded === FALSE) { // 1. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); } else { // 3. LOCK DESTINATION ROOT FOLDER TREE. if (self::acquireRootOperationLock( $copy ->id()) === FALSE) { // Very unlikely - we just created the copy, but it is already // locked by some other process? // // The copy is incomplete and disabled, but because we can't get a // lock on it we can't safely delete it, enable it, or finish the // copy. This is a mess we cannot fix. // // 1. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); throw new LockException( self::getStandardLockExceptionMessage(t( 'copied' ), $copy ->getName())); } } // // Hook & log. // ----------- // Note the change, even though descendants haven't been copied yet. self::postOperationHook( 'copy' , [ $copy , $this , $currentUserId , ]); ManageLog::activity( "Copied @kind '@name' (# @id) as '@copyName' (# @copyId)." , [ '@id' => $this ->id(), '@kind' => $this ->getKind(), '@name' => $this ->getName(), '@copyId' => $copy ->id(), '@copyName' => $copy ->getName(), 'entity' => $copy , 'uid' => $currentUserId , ]); if ( $taskNeeded === FALSE) { // This item has no descendants. Done. return $copy ; } // // Copy descendants. // ----------------- // Finishing the operation requires copying each descendant into the // new copy folder. // // 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 source root folder tree from which the copy is being made. // // - The destination root folder tree (the copy) that we need to copy into. // // These will be unlocked by a future task when the entire copy is done. $batches = []; $batches [] = [ 'sourceIds' => [(int) $this ->id()], 'destinationIds' => [(int) $copy ->id()], 'sourceRootId' => $sourceRootId , ]; $parameters = [ 'batches' => $batches , ]; $started = time(); $comments = 'Start copy to root' ; $executionTime = 0; if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) { self::processTaskCopyToRoot( $currentUserId , $parameters , $started , $comments , $executionTime , TRUE); } else { FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskInitialDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskCopyToRoot' , $currentUserId , $parameters , $started , $comments , $executionTime ); } return $copy ; } /** * Copies multiple items to the root. * * Each of the indicated items is copied. If an item is a folder, the * folder's descendants are copied as well. See copyToRoot() for * details. * * <B>Process locks</B> * The root folder tree(s) for the items being copied, and the root folder * trees of the copied items are locked during the copy. The user's root * list is locked while root-level items are being copied. * * <B>Post-operation hooks</B> * This method calls the "hook_foldershare_post_operation_copy" hook for * each item copied. * * <B>Activity log</B> * This method posts a log message after each item is copied. * * @param int[] $ids * An array of integer FolderShare entity IDs to copy. Invalid IDs * are silently skipped. * @param bool $allowRename * (optional, default = FALSE) When FALSE, each item retains its same * name as it is copied into the 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 root list. * * @throws \Drupal\foldershare\Entity\Exception\LockException * Throws an exception if this item cannot be locked for exclusive use, * or if one or more descendants cannot be locked. * @throws \Drupal\foldershare\Entity\Exception\ValidationException * Throws an exception if a name is already in use in the user's root list. * @throws \Drupal\foldershare\Entity\Exception\SystemException * Throws an exception if a serious system error occurs, such as a * file system becomes unreadable/unwritable, gets full, or gores offline. * * @see ::copyToRoot() */ public static function copyToRootMultiple( 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, it can still be copied // if (1) the original is owned by another user, and thus the copy // will go to the current user's root list, or (2) the original is // owned by this user and $allowRename is TRUE so that a new copy can // be made that doesn't collide with itself. // // 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. Lock the user's root list. // // - Check all names or create unique names. // // - For all files and folders in the same root group: Lock the shared // root, duplicate, mark disabled (if a folder). // // - After all groups: Unlock the user's root list and schedule a task // if there are descendants to copy. The task recurses to copy the // rest of each source into each copy, enables the copy, unlocks the // copy, and unlocks the source. // // ------------------------------------------------------------------. if ( empty ( $ids ) === TRUE) { // Nothing to copy. return ; } $currentUserId = 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 ->copyToRoot( '' , $allowRename ); return ; } // // Lock the user's root list. // -------------------------- // Lock the user's root list while we check for a name collision before // copying this item to the root list. // // 1. LOCK USER'S ROOT LIST. if (self::acquireUserRootListLock() === FALSE) { throw new LockException( self::getStandardLockExceptionMessage(t( 'copied' ), NULL)); } // // Group source IDs by root and validate. // -------------------------------------- // The source IDs given could be from scattered locations in different // root folder trees (or they could themselves be roots, such as roots // owned by another user). Group sources by their roots so that source // root folder locks can be done efficiently. This requires loading // each one. // // Along the way, if renaming is not allowed, check that all items have // names that will work in the user's root list as-is. // // To keep memory use down, load items only as needed and flush the // entity cache as often as practical. $names = self::findAllRootItemNames( $currentUserId ); $rootGroups = []; $itemNames = []; foreach ( $ids as $id ) { $item = self::load( $id ); if ( $item === NULL) { // The item does not exist. continue ; } if ( $allowRename === FALSE) { // // Check for in-place copies. // -------------------------- // If the item is already a root and owned by the current user, // then copying it would collide with itself. Since we cannot // rename it, just ignore it. if ( $item ->isRootItem() === TRUE && $item ->getOwnerId() === $currentUserId ) { // Issuing an error message that this combination does nothing // is not very informative. Just do nothing. continue ; } // // Check name. // ----------- // Compare the current name against the names already in use. // Abort on a collision. if (in_array( $item ->getName(), $names ) === TRUE) { // 1. UNLOCK USER'S ROOT LIST. self::releaseUserRootListLock(); throw new ValidationException( self::getStandardRenameFirstExceptionMessage( $item ->getName())); } // Add the item's name to the name list because it too is a collision // target for the next items. $names [ $item ->getName()] = (int) $item ->id(); $itemNames [(int) $item ->id()] = $item ->getName(); } else { // Create a unique name. $newName = self::createUniqueName( $names , $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 USER'S ROOT LIST. self::releaseUserRootListLock(); throw new ValidationException( self::getStandardCannotCreateUniqueNameExceptionMessage( 'copy' )); } // Add the item's name to the name list because it too is a collision // target for the next items. $names [ $newName ] = (int) $item ->id(); $itemNames [(int) $item ->id()] = $newName ; } $rootId = $item ->getRootItemId(); $rootGroups [ $rootId ][] = (int) $id ; } // // Loop through the root groups. // ----------------------------- // With items grouped by root, loop through the roots and copy the // items in batches while holding the root locked. // // On any exception, abort. $nSourceLockExceptions = 0; $batches = []; foreach ( $rootGroups as $sourceRootId => $ids ) { // // Lock source root folder tree. // ----------------------------- // Lock the root folder tree containing these items. This prevents edit // operations on them that might interfere with this operation. For // instance, this blocks delete operations that could delete the items // out from under the copy. // // 2. LOCK SOURCE ROOT FOLDER TREE. if (self::acquireRootOperationLock( $sourceRootId ) === FALSE) { ++ $nSourceLockExceptions ; continue ; } // // Duplicate WITHOUT recursion. // ---------------------------- // Duplicate each item alone, without recursing to duplicate its // possible descendants (yet). This copies the name and fields from // the original with these changes: // // - The current user is the new owner. // - The parent is the user's root list (i.e., no parent). // - The root is the user's root list (i.e., no root). // - The original name or a new unique name. // - The new item is enabled if it is not a folder. $destinationIds = []; $sourceIds = []; foreach ( $ids as $id ) { $item = self::load( $id ); if ( $item === NULL) { // The item does not exist. continue ; } // // Decide if a task will be needed. // -------------------------------- // If the item being copied is: // - A folder. // - With children. // // Then we'll need to schedule a task. $taskNeeded = ( $item ->isFolder() === TRUE && $item ->findNumberOfChildren() > 0); try { $copy = $item ->duplicateInternal( $currentUserId , self::USER_ROOT_LIST, self::USER_ROOT_LIST, $itemNames [(int) $item ->id()], ! $taskNeeded ); } catch (\Exception $e ) { // On any exception, it is not safe to continue. // // One type of exception is a system exception, which indicates a // catastrophic file system problem that has already been logged. // // 2. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); // 1. UNLOCK USER'S ROOT LIST. self::releaseUserRootListLock(); throw $e ; } // If the copy is a folder and there are possible descendants to copy, // add the item to a list for a scheduled task and lock it. if ( $taskNeeded === TRUE) { $destinationIds [] = (int) $copy ->id(); $sourceIds [] = (int) $item ->id(); // 3. LOCK NEW COPY AS ROOT FOLDER TREE. if (self::acquireRootOperationLock( $copy ->id()) === FALSE) { // Very unlikely - we just created the copy, but it is already // locked by some other process? // // The copy is incomplete and disabled, but because we can't get a // lock on it we can't safely delete it, enable it, or finish the // copy. This is a mess we cannot fix. // // 2. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); // 1. UNLOCK USER'S ROOT LIST. self::releaseUserRootListLock(); throw new LockException( self::getStandardLockExceptionMessage( t( 'copied' ), $copy ->getName())); } } // // Hook & log. // ----------- // Note the change, even though descendants haven't been copied yet. self::postOperationHook( 'copy' , [ $copy , $item , $currentUserId , ]); ManageLog::activity( "Copied @kind '@name' (# @id) as '@copyName' (# @copyId)." , [ '@id' => $item ->id(), '@kind' => $item ->getKind(), '@name' => $item ->getName(), '@copyId' => $copy ->id(), '@copyName' => $copy ->getName(), 'entity' => $copy , 'uid' => $currentUserId , ]); } // // Unlock the source root folder tree, if we're done. // -------------------------------------------------- // If none of the items copied require further work to copy their // descendants, then unlock the source root folder tree. if ( empty ( $destinationIds ) === TRUE) { // 2. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); continue ; } // // Create batch entry. // ------------------- // The rest of the copy for this batch of items must be scheduled. // Each batch shares the same source root and includes a list of // source items to copy into corresponding destination items. // // The source root cannot be unlocked until the batch is done. $batches [] = [ 'sourceRootId' => $sourceRootId , 'sourceIds' => $sourceIds , 'destinationIds' => $destinationIds , ]; } // // Unlock user's root list. // ------------------------ // The duplicates have all been created into the user's root list with // safe names. The root list lock is no longer needed. // // 1. UNLOCK USER'S ROOT LIST. self::releaseUserRootListLock(); if ( empty ( $batches ) === TRUE) { if ( $nSourceLockExceptions !== 0) { throw new LockException( self::getStandardLockExceptionMessage(t( 'copied' ), NULL)); } return ; } // // Copy descendants. // ----------------- // Finishing the operation requires copying each descendant into the // new copy folder. // // 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 source root folder tree from which the copies are being made. // // - Each of the root-level copies and their folder trees that we need // to copy into. // // These will be unlocked by a future task when the entire copy is done. $parameters = [ 'batches' => $batches , ]; $started = time(); $comments = 'Start copy to root' ; $executionTime = 0; if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) { self::processTaskCopyToRoot( $currentUserId , $parameters , $started , $comments , $executionTime , TRUE); } else { FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskInitialDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskCopyToRoot' , $currentUserId , $parameters , $started , $comments , $executionTime ); } if ( $nSourceLockExceptions !== 0) { throw new LockException( self::getStandardLockExceptionMessage(t( 'copied' ), NULL)); } } /*--------------------------------------------------------------------- * * Copy to folder. * *---------------------------------------------------------------------*/ /** * {@inheritdoc} */ public function copyToFolder( 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 the item is already a child of the destination and $allowRename // is FALSE, then the child cannot be copied without colliding with // itself. Do nothing. // // Errors: // - The new name is illegal. // - The new name is in use in the destination and renaming is not allowed. // // Actions: // - If the item is a file: Lock destination root, lock source root (if // different from destination root), check name collisions or rename, // duplicate, update destination ancestor sizes, unlock source root, // unlock destination root. // // - If the item is a folder: Lock destination root, lock source root (if // different from destination root), check name collisions or rename, // duplicate, mark disabled, update destination ancestor sizes, // and schedule task. The task recurses to copy the rest of the source // into the copy, enables the copy, unlocks source root (if // different from the destination root), and unlocks destination root. // // ------------------------------------------------------------------ // // Validate. // --------- // Confirm that the destination is a folder and that it is not a descendant // of this item. if ( $destination === NULL) { return $this ->copyToRoot( $newName , $allowRename ); } if ( $destination ->isFolder() === FALSE) { throw new ValidationException(FormatUtilities::createFormattedMessage( t( '@method was called with a copy destination that is not a folder.' , [ '@method' => __METHOD__ , ]))); } $destinationId = (int) $destination ->id(); if ( $this ->getParentFolderId() === $destinationId && $allowRename === FALSE) { // This item is already a child of the destination and we've been asked // to copy it without renaming it. That's an instant collision. // Issuing an error message that this is a problem is not very // informative. Just do nothing. return $this ; } // If the destination is a descendant of this item, then the copy // is circular. if ( $destinationId === (int) $this ->id() || $this ->isAncestorOfFolderId( $destinationId ) === TRUE) { throw new ValidationException(FormatUtilities::createFormattedMessage( t( 'The item "@name" cannot be copied into one of its own descendants.' , [ '@name' => $this ->getName(), ]))); } // // 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 destination root's folder tree. // ------------------------------------ // Lock the destination root folder tree so that other edit operations // cannot interfere with the copy. // // 1. LOCK DESTINATION ROOT FOLDER TREE. $destinationRootId = $destination ->getRootItemId(); if (self::acquireRootOperationLock( $destinationRootId ) === FALSE) { throw new LockException( self::getStandardLockExceptionMessage(t( 'copied' ), $this ->getName())); } // // Lock source root folder tree. // ----------------------------- // 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. $sourceRootId = $this ->getRootItemId(); if ( $sourceRootId !== $destinationRootId ) { // 2. LOCK SOURCE ROOT FOLDER TREE. if (self::acquireRootOperationLock( $sourceRootId ) === FALSE) { // 1. UNLOCK DESTINATION ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationRootId ); throw new LockException( self::getStandardLockExceptionMessage(t( 'copied' ), $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 ( $sourceRootId !== $destinationRootId ) { // 2. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); } // 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( 'copy' )); } } // // Decide if a task will be needed. // -------------------------------- // If the item being copied is: // - A folder. // - With children. // // Then we'll need to schedule a task. $taskNeeded = ( $this ->isFolder() === TRUE && $this ->findNumberOfChildren() > 0); // // Duplicate WITHOUT recursion. // ---------------------------- // Duplicate the item alone, without recursing to duplicate its // possible descendants (yet). This copies the name and fields from // the source with these changes: // // - The current user is the new owner. // - The parent is the destination. // - The root is the destination's root. // - The name is the original, the new name, or a created unique name. // - The new item is enabled if it is not a folder. $currentUserId = self::getCurrentUserId()[0]; try { $rootId = $destination ->getRootItemId(); $copy = $this ->duplicateInternal( $currentUserId , $destinationId , $rootId , $newName , ! $taskNeeded ); } catch (\Exception $e ) { // On any exception, it is not safe to continue. // // One type of exception is a system exception, which indicates a // catastrophic file system problem that has already been logged. if ( $sourceRootId !== $destinationRootId ) { // 2. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); } // 1. UNLOCK DESTINATION ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationRootId ); throw $e ; } // // Update sizes. // ------------- // Update the size for the destination folder and its ancestors. // Even though the descendants haven't been copied yet, the copy has // a copy of the source's size and that's enough to update the // destination. if ( $copy ->getSize() !== 0) { $destination ->updateSizeAndAncestors(); } // // Unlock the source and destination root folder trees, if we're done. // ------------------------------------------------------------------- // If there are no descendants to copy, unlock the source and // destination root folder trees. if ( $taskNeeded === FALSE) { if ( $sourceRootId !== $destinationRootId ) { // 2. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); } // 1. UNLOCK DESTINATION ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationRootId ); } // // Hook & log. // ----------- // Note the change, even though descendants haven't been copied yet. self::postOperationHook( 'copy' , [ $copy , $this , $currentUserId , ]); ManageLog::activity( "Copied @kind '@name' (# @id) as '@copyName' (# @copyId)." , [ '@id' => $this ->id(), '@kind' => $this ->getKind(), '@name' => $this ->getName(), '@copyId' => $copy ->id(), '@copyName' => $copy ->getName(), 'entity' => $copy , 'uid' => $currentUserId , ]); if ( $taskNeeded === FALSE) { // This item has no descendants. Done. return $copy ; } // // Copy descendants. // ----------------- // Finishing the operation requires copying each descendant into the // new copy folder. // // 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 source root folder tree from which the copy is being made, // if it is different from the destination root folder tree. // // - The destination root folder tree that we need to copy into. // // These will be unlocked by a future task when the entire copy is done. $batches = []; $batches [] = [ 'sourceIds' => [(int) $this ->id()], 'destinationIds' => [(int) $copy ->id()], 'sourceRootId' => $sourceRootId , ]; $parameters = [ 'batches' => $batches , 'destinationRootId' => $destinationRootId , ]; $started = time(); $comments = 'Start copy to folder' ; $executionTime = 0; if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) { self::processTaskCopyToFolder( $currentUserId , $parameters , $started , $comments , $executionTime , TRUE); } else { FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskInitialDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskCopyToFolder' , $currentUserId , $parameters , $started , $comments , $executionTime ); } return $copy ; } /** * Copies multiple items to a folder. * * Each of the indicated items is copied. If an item is a folder, the * folder's descendants are copied as well. See copyToFolder() for * details. * * <B>Process locks</B> * This item and the new destination are locked as the item is copied. This * repeats for each item copied, recursing through all children of this item. * * <B>Post-operation hooks</B> * This method calls the "hook_foldershare_post_operation_copy" hook for * each item copied. * * <B>Activity log</B> * This method posts a log message after each item is copied. * * @param int[] $ids * An array of integer FolderShare entity IDs to copy. Invalid IDs * are silently skipped. * @param \Drupal\foldershare\FolderShareInterface $destination * (optional, default = NULL = copy to the root list) The destination folder * for the copy. When NULL, the copy is added to the root list. * @param bool $allowRename * (optional, default = FALSE) When FALSE, each item retains its same * name as it is copied 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 this item cannot be locked for exclusive use, * or if one or more descendants cannot be locked. * @throws \Drupal\foldershare\Entity\Exception\ValidationException * Throws an exception if a name is already in use in the user's root list. * @throws \Drupal\foldershare\Entity\Exception\SystemException * Throws an exception if a serious system error occurs, such as a * file system becomes unreadable/unwritable, gets full, or gores offline. * * @see ::copyToFolder() */ public static function copyToFolderMultiple( 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 an item is already a child of the destination and $allowRename // is FALSE, then the child cannot be copied without colliding with // itself. Do nothing. // // Errors: // - The destination is not a folder. // - The destination is a descendant of an item (a circular copy). // - 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), duplicate, set disabled // (if a folder), and unlock shared root (if no descendants to copy). // // - After all groups: Update destination size and schedule a task if there // are any descendants to update. The task recurses through the items // copying from source to destination, enables the item, then unlocks // the source and the destination roots. // // ------------------------------------------------------------------. if ( empty ( $ids ) === TRUE) { // Nothing to copy. return ; } if ( $destination === NULL) { // If there is no destination, copy to root. self::copyToRootMultiple( $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 ->copyToFolder( $destination , '' , $allowRename ); return ; } // // Validate. // --------- // Confirm that the destination is a folder. if ( $destination ->isFolder() === FALSE) { throw new ValidationException(FormatUtilities::createFormattedMessage( t( '@method was called with a copy destination that is not a folder.' , [ '@method' => __METHOD__ , ]))); } $destinationId = (int) $destination ->id(); // // Group source IDs by root and validate. // -------------------------------------- // The source IDs given could be from scattered locations in different // root folder trees (or some or all of them may be roots themselves). // Group them by their roots so that root folder locks can be done // efficiently. // // Along the way, watch for circular moves. If renaming is not allowed, // also check that all items have names that will work in the // destination as-is. // // To keep memory use down, load items only as needed and flush the // entity cache as often as practical. $items = self::loadMultiple( $ids ); $reducedItems = []; $rootGroups = []; $currentUserId = self::getCurrentUserId()[0]; foreach ( $items as $index => $item ) { if ( $item === NULL) { // The item does not exist. continue ; } if ( $item ->getParentFolderId() === $destinationId && $allowRename === FALSE) { // This item is already a child of the destination and we've been asked // to copy it without renaming it. That's an instant collision. // Issuing an error message that this is a problem is not very // informative. Just do nothing. $items [ $index ] = NULL; continue ; } // Insure that the destination is not the same as any of the given // IDs, and is not an descendant of any of them. Failure would indicate // a circular copy of an item into itself. if ( $destinationId === (int) $item ->id() || $item ->isAncestorOfFolderId( $destinationId ) === TRUE) { throw new ValidationException(FormatUtilities::createFormattedMessage( t( 'The item "@name" cannot be copied into one of its own descendants.' , [ '@name' => $item ->getName(), ]))); } $rootId = $item ->getRootItemId(); $rootGroups [ $rootId ][] = $item ; $reducedItems [] = $item ; } if ( empty ( $rootGroups ) === TRUE) { // Nothing to copy. return ; } $items = $reducedItems ; // // Lock destination root folder tree. // ---------------------------------- // Lock the destination root folder tree so that it is prevented from // being changed by another operation. // // 1. LOCK DESTINATION ROOT FOLDER TREE. $destinationRootId = $destination ->getRootItemId(); if (self::acquireRootOperationLock( $destinationRootId ) === FALSE) { throw new LockException( self::getStandardLockExceptionMessage(t( 'copied' ), 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 item's name to the name list because it too is a collision // target for the next items. $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 ; } } // // Loop over root groups. // ---------------------- // With items grouped by root, loop through the roots and lock each // root. Duplicate each item (but not its descendants) into the destination // to provide immediate feedback to the user. Mark copied folders as // disabled so that they cannot be modified pending completion of the // copy. $nSourceLockExceptions = 0; $batches = []; foreach ( $rootGroups as $sourceRootId => $sourceItems ) { // // Lock source root folder tree. // ----------------------------- // Lock the root folder tree containing this next batch of items. // This prevents edit operations on them that might interfere with // this operation. For instance, this blocks delete operations that // could delete the items out from under the copy. if ( $sourceRootId !== $destinationRootId ) { // 2. LOCK SOURCE ROOT FOLDER TREE. if (self::acquireRootOperationLock( $sourceRootId ) === FALSE) { ++ $nSourceLockExceptions ; continue ; } } // // Duplicate WITHOUT recursion. // ---------------------------- // Duplicate each item in the batch, without recursing to duplicate its // possible descendants (yet). This copies the name and fields from // the source with these changes: // // - The specified user is the new owner. // - The parent is the user's root list (i.e., no parent). // - The root is the user's root list (i.e., no root). // - The original name or a created unique name. // - The new item is enabled if it is not a folder. $destinationIds = []; $sourceIds = []; foreach ( $sourceItems as $item ) { // // Decide if a task will be needed. // -------------------------------- // If the item being copied is: // - A folder. // - With children. // // Then we'll need to schedule a task. $taskNeeded = ( $item ->isFolder() === TRUE && $item ->findNumberOfChildren() > 0); try { $copy = $item ->duplicateInternal( $currentUserId , $destinationId , $destinationRootId , $itemNames [(int) $item ->id()], ! $taskNeeded ); } catch (\Exception $e ) { // On any exception, it is not safe to continue. // // One type of exception is a system exception, which indicates a // catastrophic file system problem that has already been logged. if ( $sourceRootId !== $destinationRootId ) { // 2. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); } // 1. UNLOCK DESTINATION ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationRootId ); throw $e ; } // If the copy is a folder (which might have decendants), we need // to finish copying as a scheduled task. Record it's ID. if ( $taskNeeded === TRUE) { $destinationIds [] = (int) $copy ->id(); $sourceIds [] = (int) $item ->id(); } // // Hook & log. // ----------- // Note the change, even though descendants haven't been copied yet. self::postOperationHook( 'copy' , [ $copy , $item , $currentUserId , ]); ManageLog::activity( "Copied @kind '@name' (# @id) as '@copyName' (# @copyId)." , [ '@id' => $item ->id(), '@kind' => $item ->getKind(), '@name' => $item ->getName(), '@copyId' => $copy ->id(), '@copyName' => $copy ->getName(), 'entity' => $copy , 'uid' => $currentUserId , ]); } // // Unlock the source root folder tree, if we're done. // -------------------------------------------------- // If none of the copied items require further work to copy descendants, // then we're done with the source root folder tree. if ( empty ( $destinationIds ) === TRUE) { // 2. UNLOCK SOURCE ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); continue ; } // // Create batch entry. // ------------------- // The rest of the copy for this batch of items must be scheduled. // Each batch shares the same source root and includes a list of // source items to copy into corresponding destination items. // // The source root cannot be unlocked until the batch is done. $batches [] = [ 'sourceRootId' => $sourceRootId , 'sourceIds' => $sourceIds , 'destinationIds' => $destinationIds , ]; } // // Update ancestor sizes. // ---------------------- // Update destination ancestor sizes. // Even though the descendants haven't been copied yet, each copy has // a copy of the source's size and that's enough to update the // destination. if ( $copy ->getSize() !== 0) { $destination ->updateSizeAndAncestors(); } if ( empty ( $batches ) === TRUE) { // 1. UNLOCK DESTINATION'S ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationRootId ); if ( $nSourceLockExceptions !== 0) { throw new LockException( self::getStandardLockExceptionMessage(t( 'copied' ), NULL)); } return ; } // // Copy descendants. // ----------------- // Finishing the operation requires copying each descendant into the // new copy folder. // // 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 root folder tree into which the copies are placed. // // - Each source root folder tree from which the copies are being made. // // These will be unlocked by a future task when the entire copy is done. $parameters = [ 'batches' => $batches , 'destinationRootId' => $destinationRootId , ]; $started = time(); $comments = 'Start copy to folder' ; $executionTime = 0; if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) { self::processTaskCopyToFolder( $currentUserId , $parameters , $started , $comments , $executionTime , TRUE); } else { FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskInitialDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskCopyToFolder' , $currentUserId , $parameters , $started , $comments , $executionTime ); } if ( $nSourceLockExceptions !== 0) { throw new LockException( self::getStandardLockExceptionMessage(t( 'copied' ), NULL)); } } /*--------------------------------------------------------------------- * * Copy implementation. * *---------------------------------------------------------------------*/ /** * Duplicates this item, but not its children. * * <B>This method is internal and strictly for use by the FolderShare * module itself.</B> * * The caller MUST have locked the root folder trees for this item and * the destination. * * The item is copied and its owner ID, parent folder ID, root ID, name, * and disabled flag are updated to the given values. Access grants are * reset to defaults and the item is saved. * * If the item wraps an underlying File or Media entity, that entity is * duplicated as well. * * <B>Process locks</B> * This item does not lock anything, but the caller MUST have locked * the root folder trees for this item and the destination. * * @param int $newOwnerUid * The user ID for the owner of the new duplicate entity. * @param int $newParentId * The duplicate's parent folder ID. USER_ROOT_LIST indicates it has * no parent and is a root item in the user's root list. * @param int $newRootId * The duplicate's root ID. USER_ROOT_LIST indicates it is a root item * in the user's root list. * @param string $newName * The duplicate's new name. This may be the same as the original's name. * @param bool $enabled * (optional, default = TRUE) Whether to mark the item enabled. * * @return \Drupal\foldershare\Entity\FolderShare * Returns the completed duplicate. * * @throws \Drupal\foldershare\Entity\Exception\SystemException * For files, throws an exception if a serious system error occurs while * duplicating the underlying local file. System errors may indicate a * file system has become unreadable/unwritable, is full, or is offline. * * @see ::copyToRoot() * @see ::copyToFolder() */ private function duplicateInternal( int $newOwnerUid , int $newParentId , int $newRootId , string $newName , bool $enabled = TRUE) { // // Duplicate entity. // ----------------- // Create a new entity initialized with copies of the original's fields. // Then update the owner, parent, root, and name. // // Clear access grants. If the new entity is going into a root list, // default access grants are added automatically for the owner. // Always clear the file, image, and media IDs, then create appropriate // duplicates below. $copy = parent::createDuplicate(); $copy ->setOwnerIdInternal( $newOwnerUid ); $copy ->setParentFolderId( $newParentId ); $copy ->setRootItemId( $newRootId ); $copy ->setName( $newName ); $copy ->clearAccessGrants(); $copy ->setSystemDisabled(! $enabled ); $copy ->setFileId(-1); $copy ->setImageId(-1); $copy ->setMediaId(-1); // // Copy wrapped entities, if any. // ---------------------------- // If the item wraps another entity, duplicate it too. try { // File field. $file = $this ->getFile(); if ( $file !== NULL) { // Duplicate the file entity, then set the new owner and name. // // File duplication can encounter a variety of catastrophic file // system problems, such as issues with directory permissions or // a storage device that goes offline. In such cases, the call // posts an emergency log message and throws an exception. $newFile = self::duplicateFileEntityInternal( $file , $newName , $newOwnerUid ); unset( $file ); $copy ->setFileId( $newFile ->id()); unset( $newFile ); } // Image field. $file = $this ->getImage(); if ( $file !== NULL) { // Duplicate the file entity, then set the new owner and name. // // File duplication can encounter a variety of catastrophic file // system problems, such as issues with directory permissions or // a storage device that goes offline. In such cases, the call // posts an emergency log message and throws an exception. $newFile = self::duplicateFileEntityInternal( $file , $newName , $newOwnerUid ); unset( $file ); $copy ->setImageId( $newFile ->id()); unset( $newFile ); } // Media field. $media = $this ->getMedia(); if ( $media !== NULL) { $newMedia = $media ->createDuplicate(); $newMedia ->setOwnerId( $newOwnerUid ); $newMedia ->setName( $newName ); $newMedia ->save(); unset( $media ); $copy ->setMediaId( $newMedia ->id()); unset( $newMedia ); } $copy ->save(); // Garbage collect. For files and media, File or Media entities have // been loaded, duplicated, and released. Flush them from memory ASAP. gc_collect_cycles(); return $copy ; } catch (\Exception $e ) { // On any error, delete the newly created duplicate entity above // since it cannot be finished. $copy -> delete (); throw $e ; } } /** * Copies this file or folder into a parent folder, recursing as needed. * * <B>This method is internal and strictly for use by the FolderShare * module itself.</B> * * The caller MUST have locked the root folder trees for this item and * the destination. This prevents interference in either the source tree * or the destination during the copy. * * If an item with the same name does not exist already in the destination, * it is created and copying recurses into children, if any. * * If an item with the same name already exists in the destination, it may be * from a previous copy that was interrupted. If that copy is enabled, that * previous copy finished and this function returns. * * Otherwise, a previous copy left a disabled folder in the destination to * indicate an incomplete copy. Thisfunction recurses into that folder * and completes the copy. * * Whenever a folder is copied, it is initially disabled, then reenabled * after all children have been copied. * * <B>Process locks</B> * This item does not lock anything, but the caller MUST have locked * the root folder trees for this item and the destination. * * <B>Post-operation hooks</B> * This method calls the "hook_foldershare_post_operation_copy" hook for * each item copied. * * <B>Activity log</B> * This method post a log message after each item is copied. * * @param \Drupal\foldershare\FolderShareInterface $destination * The destination folder. * @param int $newOwnerUid * The user ID of the owner of the new items from the copy. * @param int $opCounter * A counter that increments each time a load or copy/save 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. * * @return \Drupal\foldershare\FolderShareInterface * Returns the new copy. * * @throws \Drupal\foldershare\Entity\Exception\LockException * Throws an exception if an access lock could not be acquired. * @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. * @throws \Drupal\foldershare\Entity\Exception\SystemException * Throws an exception if a serious system error occurs, such as a * file system becomes unreadable/unwritable, gets full, or goes offline. * * @see ::copyToRoot() * @see ::copyToFolder() */ private function copyToFolderInternal( FolderShareInterface $destination , int $newOwnerUid , int & $opCounter , bool $interactive = FALSE) { // Check if done. // -------------- // Look for an item with the same name in the destination. There are // three possibilities: // // - Copy does not exist. The copy hasn't been created yet, so continue. // // - Copy exists and is disabled. The copy was started but did not finish, // so continue. // // - Copy exists and is enabled. The copy finished, so return. $copy = NULL; $copyId = self::findNamedChildId( $destination ->id(), $this ->getName()); if ( $copyId !== FALSE) { // There is a copy already in the destination. If the // copy can be loaded (and it would be odd if it can't be) and it // is NOT disabled, then the copy was completed by prior activity. // We're done. $copy = self::load( $copyId ); if ( $copy !== NULL) { // Increment to count load. ++ $opCounter ; if ( $copy ->isSystemDisabled() === FALSE) { return $copy ; } } } if ( $newOwnerUid === (-1)) { $newOwnerUid = (int) \Drupal::currentUser()->id(); } // // Duplicate WITHOUT recursion. // ---------------------------- // If there is no copy yet, create one by duplicating this item. $sourceChildrenIds = $this ->findChildrenIds(); if ( $copy === NULL) { // This item alone is duplicated, without recursing to duplicate // descendants (yet). The duplicate copies the name and fields from the // original with these changes: // - The current user is the new owner. // - The destination is the new parent. // - The root is the new destination's root. // - The name is the original. // - The new item (if it is a folder) is disabled until we finish // copying children below. If there are no children, enable it. try { $copy = $this ->duplicateInternal( $newOwnerUid , (int) $destination ->id(), $destination ->getRootItemId(), $this ->getName(), ( $this ->isFolder() === FALSE || empty ( $sourceChildrenIds ) === TRUE)); // Increment to count copy. ++ $opCounter ; } catch (SystemException $e ) { // A file could not be copied. // // On a system exception, the copy aborts while trying to create a // file. We cannot finish it because we cannot fix the underlying // system problem. Nothing more can be copied. // // The problem has been reported to the system log, but not to // the user. It is unclear how to do that well. throw $e ; } catch (\Exception $e ) { // Unknown error. It may not be safe to continue, so abort. throw $e ; } } // // Hook & log. // ----------- // Note the change, even though descendants haven't been copied yet. self::postOperationHook( 'copy' , [ $copy , $this , $newOwnerUid , ]); ManageLog::activity( "Copied @kind '@name' (# @id) as '@copyName' (# @copyId)." , [ '@id' => $this ->id(), '@kind' => $this ->getKind(), '@name' => $this ->getName(), '@copyId' => $copy ->id(), '@copyName' => $copy ->getName(), 'entity' => $copy , 'uid' => $newOwnerUid , ]); // // Copy children. // -------------- // If this item has no children, there is nothing further to copy. // Otherwise recurse to copy children. if ( empty ( $sourceChildrenIds ) === TRUE) { if ( $opCounter >= self::USAGE_CHECK_INTERVAL) { // Garbage collect. Copying has created some temporary objects. // Flush them from memory. gc_collect_cycles(); 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; } return $copy ; } foreach ( $sourceChildrenIds as $sourceChildId ) { $sourceChild = self::load( $sourceChildId ); if ( $sourceChild === NULL) { // The child does not exist. continue ; } // Increment to count load. ++ $opCounter ; try { $newChild = $sourceChild ->copyToFolderInternal( $copy , $newOwnerUid , $opCounter , $interactive ); } catch (ExecutionTimeLimitException | MemoryLimitException $e ) { // An execution time or memory limit has been exceeded. throw $e ; } catch (SystemException $e ) { // A file could not be copied. Leave it disabled. // // On a system exception, the copy aborts while trying to create a // file. We cannot finish it because we cannot fix the underlying // system problem. Nothing more can be copied. // // The problem has been reported to the system log, but not to // the user. It is unclear how to do that well. throw $e ; } unset( $sourceChild ); unset( $newChild ); } unset( $sourceChildrenIds ); // The copy is complete. Mark the folder enabled. $copy ->setSystemDisabled(FALSE); $copy ->save(); // Increment to count save. ++ $opCounter ; if ( $opCounter >= self::USAGE_CHECK_INTERVAL) { // Garbage collect. Copying has created some temporary objects. // Flush them from memory. gc_collect_cycles(); 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; } return $copy ; } /*--------------------------------------------------------------------- * * Background task handling. * *---------------------------------------------------------------------*/ /** * Processes a copy-to-root task from the scheduled task queue. * * <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 copy-to-root task provides a list of source IDs for entities being * copied, a list of destination IDs to copy into, and the ID of the * source's root to unlock upon completion. * * <B>Post-operation hooks</B> * This method calls the "hook_foldershare_post_operation_copy" hook for * each item copied. * * <B>Activity log</B> * This method posts a log message afte each item is copied. * * @param int $requestingUid * The user ID of the user that requested the delete. This is ignored. * @param array $parameters * The queued task's perameters. This is an associative array with keys: * - 'batches': an array of batches. Each batch is an associative array * with keys: * - 'sourceIds': an array of IDs of source entities to be copied. * - 'destinationIds': an array of IDs of destination entities to be * copied into. There is one destination for each source. Every * destination is a root folder. * - 'sourceRootId': the ID of the root folder tree containing the * sources for the batch's copy. * @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. */ public static function processTaskCopyToRoot( int $requestingUid , array $parameters , int $started , string $comments , int $executionTime , bool $interactive = FALSE) { // // Validate. // --------- // The parametes array must contain a batches array that has a list // of source IDs, destination IDs, and a source root ID. if (isset( $parameters [ 'batches' ]) === FALSE || is_array ( $parameters [ 'batches' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , 'batches' ); return ; } $batches = $parameters [ 'batches' ]; foreach ( $batches as $index => $batch ) { if (isset( $batch [ 'sourceIds' ]) === FALSE || is_array ( $batch [ 'sourceIds' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , "batches[$index][sourceIds]" ); return ; } if (isset( $batch [ 'destinationIds' ]) === FALSE || is_array ( $batch [ 'destinationIds' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , "batches[$index][destinationIds]" ); return ; } if (isset( $batch [ 'sourceRootId' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , "batches[$index][sourceRootId]" ); return ; } $sourceIds = $batch [ 'sourceIds' ]; $destinationIds = $batch [ 'destinationIds' ]; if ( count ( $sourceIds ) !== count ( $destinationIds )) { ManageLog::error( "Programmer error: Entity ID lists are malformed for internal '@taskName' performing a multi-step copy.\nThe '@parameter1Name' and '@parameter2Name' parameter lists must be the same size, but they are not." , [ '@taskName' => __METHOD__ , '@parameter1Name' => "batch[$index][sourceIds]" , '@parameter2Name' => "batch[$index][destinationIds]" , ]); return ; } } // // 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::processTaskCopyToRoot' , $requestingUid , [ 'batches' => $batches , ], $started , 'Safety-net requeue' , $executionTime ); // // Prepare. // -------- // Garbage collect and initialize. $beginTime = time(); $opCounter = 0; gc_collect_cycles(); // // Loop through each batch. // ------------------------ // All of the batch entries copy from the same root folder tree, but // copy different source folders into corresponding destination folders. // Every destination folder is a root (because this operation was // initiated by copyToRoot()). // // For each source and destination, both already exist and we now // need to copy the source's children to the destination. // // Recursively copy each item in each batch. As a batch is finished, // unlock the batch's source and destination root folder trees. foreach ( $batches as $batchIndex => $batch ) { $sourceIds = $batch [ 'sourceIds' ]; $destinationIds = $batch [ 'destinationIds' ]; $sourceRootId = (int) $batch [ 'sourceRootId' ]; // // Copy source descendants to destination. // --------------------------------------- // Loop through the source-destination pair. For each source child, // copy it into the destination, recursing as needed. // // Copying returns immediately if an item already exists in the // destination. This may mean a prior run of this task, or another // process, has already done the copy and it can be silently skipped. foreach ( $sourceIds as $index => $sourceId ) { $source = self::load((int) $sourceId ); if ( $source === NULL) { // Source does not exist. unset( $sourceIds [ $index ]); unset( $destinationIds [ $index ]); continue ; } // Increment to count load. ++ $opCounter ; $destinationId = (int) $destinationIds [ $index ]; $destination = self::load( $destinationId ); if ( $destination === NULL) { // Destination does not exist. unset( $sourceIds [ $index ]); unset( $destinationIds [ $index ]); continue ; } // Increment to count load. ++ $opCounter ; $sourceChildrenIds = $source ->findChildrenIds(); unset( $source ); foreach ( $sourceChildrenIds as $sourceChildId ) { $sourceChild = self::load( $sourceChildId ); if ( $sourceChild === NULL) { // The child does not exist. continue ; } // Increment to count load. ++ $opCounter ; try { $newChild = $sourceChild ->copyToFolderInternal( $destination , $requestingUid , $opCounter , $interactive ); unset( $sourceChild ); unset( $newChild ); } 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 source or destination locks. These were // locked before the original task was scheduled and still // apply. // // Schedule continuation task. Execution has already unset // $batches array entries as a batch is completed. Execution // has also unset $sourceIds and $destinationIds array entries // as they are completed. Update the current $batches array // entry with those, then schedule whatever is left to do. $reason = ( $e instanceof ExecutionTimeLimitException) ? 'time limit' : 'memory use limit' ; $batches [ $batchIndex ] = [ 'sourceIds' => $sourceIds , 'destinationIds' => $destinationIds , 'sourceRootId' => $sourceRootId , ]; FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskContinuationDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskCopyToRoot' , $requestingUid , [ 'batches' => $batches , ], $started , "Continuation due to $reason after $opCounter ops" , $executionTime + (time() - $beginTime )); // Delete the safety net task. FolderShareScheduledTask::deleteTask( $safetyNetTask ); return ; } catch (\Exception $e ) { // An error occurred. // // One type of error is a system exception, which indicates a // catastrophic file system problem that has already been logged. // // There's not much we can do. } } // The copy is done. Enable the destination. $destination ->setSystemDisabled(FALSE); $destination ->save(); // Increment to count save. ++ $opCounter ; unset( $destination ); // UNLOCK DESTINATION'S ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationId ); unset( $sourceIds [ $index ]); unset( $destinationIds [ $index ]); } // UNLOCK SOURCE'S ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); unset( $batch [ $batchIndex ]); // Garbage collect. gc_collect_cycles(); } // Delete the safety net task. FolderShareScheduledTask::deleteTask( $safetyNetTask ); } /** * Processes a copy task from the scheduled task queue. * * <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 copy task provides a list of IDs for entities to copy. For * entities that are folders, all of their descendants are copied as well. * * <B>Post-operation hooks</B> * This method calls the "hook_foldershare_post_operation_copy" hook for * each item copied. * * <B>Activity log</B> * This method posts a log message after each item is copied. * * @param int $requestingUid * The user ID of the user that requested the delete. This is ignored. * @param array $parameters * The queued task's perameters. This is an associative array with keys: * - 'destinationRootId': the ID of the root folder tree containing the * destinations of the copy. * - 'batches': an array of batches. Each batch is an associative array * with keys: * - 'sourceIds': an array of IDs of source entities to be copied. * - 'destinationIds': an array of IDs of destination entities to be * copied into. There is one destination for each source. * - 'sourceRootId': the ID of the root folder tree containing the * sources for the batch's copy. * @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. */ public static function processTaskCopyToFolder( int $requestingUid , array $parameters , int $started , string $comments , int $executionTime , bool $interactive = FALSE) { // // Validate. // --------- // The parametes array must contain a batches array that has a list // of source IDs, destination IDs, and a source root ID. A final // destination root ID is also required. if (isset( $parameters [ 'batches' ]) === FALSE || is_array ( $parameters [ 'batches' ]) === FALSE) { ManagELog::missingTaskParameter( __METHOD__ , 'batches' ); return ; } if (isset( $parameters [ 'destinationRootId' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , 'destinationRootId' ); return ; } $batches = $parameters [ 'batches' ]; $destinationRootId = (int) $parameters [ 'destinationRootId' ]; foreach ( $batches as $index => $batch ) { if (isset( $batch [ 'sourceIds' ]) === FALSE || is_array ( $batch [ 'sourceIds' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , "batches[$index][sourceIds]" ); return ; } if (isset( $batch [ 'destinationIds' ]) === FALSE || is_array ( $batch [ 'destinationIds' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , "batches[$index][destinationIds]" ); return ; } if (isset( $batch [ 'sourceRootId' ]) === FALSE) { ManageLog::missingTaskParameter( __METHOD__ , "batches[$index][sourceRootId]" ); return ; } $sourceIds = $batch [ 'sourceIds' ]; $destinationIds = $batch [ 'destinationIds' ]; $sourceRootId = (int) $batch [ 'sourceRootId' ]; if ( count ( $sourceIds ) !== count ( $destinationIds )) { ManageLog::error( "Programmer error: Entity ID lists are malformed for internal '@taskName' performing a multi-step copy.\nThe '@parameter1Name' and '@parameter2Name' parameter lists be the same size, but they are not." , [ '@taskName' => __METHOD__ , '@parameter1Name' => "batch[$index][sourceIds]" , '@parameter2Name' => "batch[$index][destinationIds]" , ]); return ; } } // // 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::processTaskCopyToFolder' , $requestingUid , [ 'batches' => $batches , 'destinationRootId' => $destinationRootId , ], $started , 'Safety-net requeue' , $executionTime ); // // Prepare. // -------- // Garbage collect and initialize. $beginTime = time(); $opCounter = 0; gc_collect_cycles(); // // Loop through each batch. // ------------------------ // All of the batch entries copy into the same destination, but each // batch copies from a different source root folder tree. // // Recursively copy each item in each batch. As a batch is finished, // unlock the batch's source root folder tree. foreach ( $batches as $batchIndex => $batch ) { $sourceIds = $batch [ 'sourceIds' ]; $destinationIds = $batch [ 'destinationIds' ]; $sourceRootId = (int) $batch [ 'sourceRootId' ]; // // Copy source descendants to destination. // --------------------------------------- // Loop through the source-destination pair. For each source child, // copy it into the destination, recursing as needed. // // Copying returns immediately if an item already exists in the // destination. This may mean a prior run of this task, or another // process, has already done the copy and it can be silently skipped. foreach ( $sourceIds as $index => $sourceId ) { $source = self::load((int) $sourceId ); if ( $source === NULL) { // Source does not exist. unset( $sourceIds [ $index ]); unset( $destinationIds [ $index ]); continue ; } // Increment to count load. ++ $opCounter ; $destinationId = (int) $destinationIds [ $index ]; $destination = self::load( $destinationId ); if ( $destination === NULL) { // Destination does not exist. unset( $sourceIds [ $index ]); unset( $destinationIds [ $index ]); continue ; } // Increment to count load. ++ $opCounter ; $sourceChildrenIds = $source ->findChildrenIds(); unset( $source ); foreach ( $sourceChildrenIds as $sourceChildId ) { $sourceChild = self::load( $sourceChildId ); if ( $sourceChild === NULL) { // The child does not exist. continue ; } // Increment to count load. ++ $opCounter ; try { $newChild = $sourceChild ->copyToFolderInternal( $destination , $requestingUid , $opCounter , $interactive ); unset( $sourceChild ); unset( $newChild ); } 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 source or destination locks. These were // locked before the original task was scheduled and still // apply. // // Schedule continuation task. Execution has already unset // $batches array entries as a batch is completed. Execution // has also unset $sourceIds and $destinationIds array entries // as they are completed. Update the current $batches array // entry with those, then schedule whatever is left to do. $reason = ( $e instanceof ExecutionTimeLimitException) ? 'time limit' : 'memory use limit' ; $batches [ $batchIndex ] = [ 'sourceIds' => $sourceIds , 'destinationIds' => $destinationIds , 'sourceRootId' => $sourceRootId , ]; FolderShareScheduledTask::createTask( time() + Settings::getScheduledTaskContinuationDelay(), '\Drupal\foldershare\Entity\FolderShare::processTaskCopyToFolder' , $requestingUid , [ 'batches' => $batches , 'destinationRootId' => $destinationRootId , ], $started , "Continuation due to $reason after $opCounter ops" , $executionTime + (time() - $beginTime )); // Delete the safety net task. FolderShareScheduledTask::deleteTask( $safetyNetTask ); return ; } catch (\Exception $e ) { // An error occurred. // // One type of error is a system exception, which indicates a // catastrophic file system problem that has already been logged. // // There's not much we can do. } } // The copy is done. Enable the destination. $destination ->setSystemDisabled(FALSE); $destination ->save(); // Increment to count save. ++ $opCounter ; unset( $destination ); unset( $sourceIds [ $index ]); unset( $destinationIds [ $index ]); } if ( $sourceRootId !== $destinationRootId ) { // UNLOCK SOURCE'S ROOT FOLDER TREE. self::releaseRootOperationLock( $sourceRootId ); } unset( $batches [ $batchIndex ]); // Garbage collect. gc_collect_cycles(); } // UNLOCK DESTINATION'S ROOT FOLDER TREE. self::releaseRootOperationLock( $destinationRootId ); // Delete the safety net task. FolderShareScheduledTask::deleteTask( $safetyNetTask ); } } |