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);
  }
 
}

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

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