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