Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bcf3a0bcc | |||
| 2477163910 | |||
| 77611110d6 | |||
| 0d48c8cfe3 | |||
| b697547afd | |||
| 05768e230d | |||
| 831e780071 | |||
| a1be9cdbe9 | |||
| c92bfe4339 | |||
| 18d9d06efe | |||
| 778f87aed3 | |||
| 88d0df82c2 | |||
| 447b39e3b4 | |||
| a66bf500ec | |||
| 9e911c7973 | |||
| 85d8df549b | |||
| 0e9c3905a0 | |||
| ba2df3f7c1 | |||
| f66d375c95 | |||
| 05c2491d7f | |||
| 784faa4d71 | |||
| a0c08267f6 |
@@ -38,8 +38,14 @@ use Sabre\DAV\Exception\NotFound;
|
||||
use Sabre\DAV\Exception\ServiceUnavailable;
|
||||
use Sabre\DAV\IFile;
|
||||
use Sabre\DAV\INode;
|
||||
use Sabre\DAV\INodeByPath;
|
||||
|
||||
class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget {
|
||||
class Directory extends Node implements
|
||||
\Sabre\DAV\ICollection,
|
||||
\Sabre\DAV\IQuota,
|
||||
\Sabre\DAV\IMoveTarget,
|
||||
\Sabre\DAV\ICopyTarget,
|
||||
INodeByPath {
|
||||
/**
|
||||
* Cached directory content
|
||||
* @var FileInfo[]
|
||||
@@ -490,4 +496,59 @@ class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuot
|
||||
public function getNode(): Folder {
|
||||
return $this->node;
|
||||
}
|
||||
|
||||
public function getNodeForPath($path) {
|
||||
return $this->getNodeForPathInternal($path);
|
||||
}
|
||||
|
||||
private function getNodeForPathInternal($path, bool $includeChildren = true) {
|
||||
$nodeIsRoot = $this->path === '/';
|
||||
$path = ltrim($path, '/');
|
||||
$fullPath = $nodeIsRoot ? $this->path . $path : $this->path . '/' . $path;
|
||||
|
||||
try {
|
||||
[$destinationDir, $destinationName] = \Sabre\Uri\split($fullPath);
|
||||
$this->fileView->verifyPath($destinationDir, $destinationName, true);
|
||||
$info = $this->fileView->getFileInfo($fullPath, $includeChildren);
|
||||
} catch (StorageNotAvailableException $e) {
|
||||
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), 0, $e);
|
||||
} catch (InvalidPathException $ex) {
|
||||
throw new InvalidPath($ex->getMessage(), false, $ex);
|
||||
} catch (ForbiddenException $e) {
|
||||
throw new \Sabre\DAV\Exception\Forbidden($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
if (!$info) {
|
||||
throw new \Sabre\DAV\Exception\NotFound('File with name ' . $fullPath
|
||||
. ' could not be located');
|
||||
}
|
||||
|
||||
if (!$info->isReadable()) {
|
||||
if (Server::get(IAppManager::class)->isEnabledForAnyone('files_accesscontrol')) {
|
||||
throw new Forbidden('No read permissions. This might be caused by files_accesscontrol, check your configured rules');
|
||||
}
|
||||
|
||||
throw new Forbidden('No read permissions');
|
||||
}
|
||||
|
||||
if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
|
||||
$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
|
||||
} else {
|
||||
// In case reading a directory was allowed but it turns out the node was a not a directory, reject it now.
|
||||
if (!$this->info->isReadable()) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$node = new File($this->fileView, $info, $this->shareManager);
|
||||
}
|
||||
$this->tree?->cacheNode($node);
|
||||
|
||||
if ($destinationDir !== '') {
|
||||
// recurse upwards until the root (for backwards-compatibility)
|
||||
// no need to get child information
|
||||
$this->getNodeForPathInternal($destinationDir, false);
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ use OCP\Server;
|
||||
use OCP\Share\Exceptions\ShareNotFound;
|
||||
use OCP\Share\IManager;
|
||||
|
||||
abstract class Node implements \Sabre\DAV\INode {
|
||||
abstract class Node extends \Sabre\DAV\Node implements \Sabre\DAV\INode {
|
||||
/**
|
||||
* The path to the current node
|
||||
*
|
||||
|
||||
@@ -748,6 +748,45 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
|
||||
return $shares;
|
||||
}
|
||||
|
||||
public function getSharedWithByNodeIds(
|
||||
$userId,
|
||||
$shareType,
|
||||
$nodeIds,
|
||||
$limit,
|
||||
$offset
|
||||
) {
|
||||
/** @var IShare[] $shares */
|
||||
$shares = [];
|
||||
|
||||
//Get shares directly with this user
|
||||
$qb = $this->dbConnection->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('share');
|
||||
|
||||
// Order by id
|
||||
$qb->orderBy('id');
|
||||
|
||||
// Set limit and offset
|
||||
if ($limit !== -1) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
$qb->setFirstResult($offset);
|
||||
|
||||
$qb->where($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY)));
|
||||
$qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)));
|
||||
$qb->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($nodeIds, IQueryBuilder::PARAM_INT_ARRAY)));
|
||||
|
||||
$cursor = $qb->executeQuery();
|
||||
|
||||
while ($data = $cursor->fetch()) {
|
||||
$shares[] = $this->createShareObject($data);
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
|
||||
|
||||
return $shares;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a share by token
|
||||
*
|
||||
|
||||
+65
-2
@@ -7,16 +7,23 @@
|
||||
*/
|
||||
namespace OCA\Files_Sharing\External;
|
||||
|
||||
use LogicException;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use OCP\Files\Config\IMountProvider;
|
||||
use OCP\Files\Config\IPartialMountProvider;
|
||||
use OCP\Files\Storage\IStorageFactory;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUser;
|
||||
use OCP\Server;
|
||||
use OCP\Share\IShare;
|
||||
use function count;
|
||||
use function str_starts_with;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
class MountProvider implements IMountProvider {
|
||||
class MountProvider implements IMountProvider, IPartialMountProvider {
|
||||
public const STORAGE = '\OCA\Files_Sharing\External\Storage';
|
||||
|
||||
/**
|
||||
@@ -54,7 +61,7 @@ class MountProvider implements IMountProvider {
|
||||
$qb->select('remote', 'share_token', 'password', 'mountpoint', 'owner')
|
||||
->from('share_external')
|
||||
->where($qb->expr()->eq('user', $qb->createNamedParameter($user->getUID())))
|
||||
->andWhere($qb->expr()->eq('accepted', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT)));
|
||||
->andWhere($qb->expr()->eq('accepted', $qb->createNamedParameter(IShare::STATUS_ACCEPTED, IQueryBuilder::PARAM_INT)));
|
||||
$result = $qb->executeQuery();
|
||||
$mounts = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
@@ -65,4 +72,60 @@ class MountProvider implements IMountProvider {
|
||||
$result->closeCursor();
|
||||
return $mounts;
|
||||
}
|
||||
|
||||
public function getMountsFromMountPoints(
|
||||
string $path,
|
||||
array $mountProviderArgs,
|
||||
IStorageFactory $loader,
|
||||
): array {
|
||||
if (\empty($mountProviderArgs)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$uniqueMountOwnerIds = [];
|
||||
$user = null;
|
||||
foreach ($mountProviderArgs as $mountProviderArg) {
|
||||
$mountInfo = $mountProviderArg->mountInfo;
|
||||
// get a list of unique owner IDs root mount IDs
|
||||
$user ??= $mountInfo->getUser();
|
||||
$uniqueMountOwnerIds[$user->getUID()] ??= true;
|
||||
}
|
||||
$uniqueMountOwnerIds = array_keys($uniqueMountOwnerIds);
|
||||
|
||||
// make sure the MPs belong to the same user
|
||||
if ($user === null || count($uniqueMountOwnerIds) !== 1) {
|
||||
// question: what kind of exception to throw in here?
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$mountOwnerId = $user->getUID();
|
||||
$pathPrefix = "/$mountOwnerId/files";
|
||||
$pathHashes = [];
|
||||
foreach ($mountProviderArgs as $mountProviderArg) {
|
||||
$mountPoint =
|
||||
rtrim($mountProviderArg->mountInfo->getMountPoint(), '/');
|
||||
if (str_starts_with($mountPoint, $pathPrefix)) {
|
||||
$pathHashes[] = md5(substr($mountPoint, strlen($pathPrefix)));
|
||||
}
|
||||
}
|
||||
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->select('remote', 'share_token', 'password', 'mountpoint', 'owner')
|
||||
->from('share_external')
|
||||
->where($qb->expr()->eq('user', $qb->createNamedParameter($user->getUID())))
|
||||
->andWhere($qb->expr()->in('mountpoint_hash',
|
||||
$qb->createNamedParameter($pathHashes, IQueryBuilder::PARAM_STR_ARRAY)))
|
||||
->andWhere($qb->expr()->eq('accepted', $qb->createNamedParameter
|
||||
(IShare::STATUS_ACCEPTED, IQueryBuilder::PARAM_INT)));
|
||||
$result = $qb->executeQuery();
|
||||
$mounts = [];
|
||||
while ($row = $result->fetch()) {
|
||||
$row['manager'] = $this;
|
||||
$row['token'] = $row['share_token'];
|
||||
$mount = $this->getMount($user, $row, $loader);
|
||||
$mounts[$mount->getMountPoint()] = $mount;
|
||||
}
|
||||
$result->closeCursor();
|
||||
return $mounts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,22 +7,57 @@
|
||||
*/
|
||||
namespace OCA\Files_Sharing;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use OC\Files\View;
|
||||
use OCA\Files_Sharing\Event\ShareMountedEvent;
|
||||
use OCA\Talk\Share\RoomShareProvider;
|
||||
use OCP\Cache\CappedMemoryCache;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Files\Config\IMountProvider;
|
||||
use OCP\Files\Config\IPartialMountProvider;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\Mount\IMountManager;
|
||||
use OCP\Files\Mount\IMountPoint;
|
||||
use OCP\Files\Storage\IStorageFactory;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUser;
|
||||
use OCP\Share\IAttributes;
|
||||
use OCP\Share\IManager;
|
||||
use OCP\Share\IShare;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use function array_filter;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
class MountProvider implements IMountProvider, IPartialMountProvider {
|
||||
|
||||
private const SUPPORTED_SHARE_TYPES = [
|
||||
IShare::TYPE_USER,
|
||||
IShare::TYPE_GROUP,
|
||||
IShare::TYPE_USERGROUP,
|
||||
RoomShareProvider::SHARE_TYPE_USERROOM,
|
||||
IShare::TYPE_CIRCLE,
|
||||
IShare::TYPE_ROOM,
|
||||
IShare::TYPE_DECK,
|
||||
IShare::TYPE_SCIENCEMESH,
|
||||
];
|
||||
|
||||
/**
|
||||
* Maps internal share types to the right provider
|
||||
*
|
||||
* NOTE: this information should come either from the provider or from
|
||||
* the provider factory!
|
||||
*/
|
||||
private const TYPE_MAPPING = [
|
||||
IShare::TYPE_USERGROUP => IShare::TYPE_GROUP,
|
||||
RoomShareProvider::SHARE_TYPE_USERROOM => IShare::TYPE_ROOM,
|
||||
];
|
||||
|
||||
class MountProvider implements IMountProvider {
|
||||
/**
|
||||
* @param IConfig $config
|
||||
* @param IManager $shareManager
|
||||
@@ -35,6 +70,8 @@ class MountProvider implements IMountProvider {
|
||||
protected IEventDispatcher $eventDispatcher,
|
||||
protected ICacheFactory $cacheFactory,
|
||||
protected IMountManager $mountManager,
|
||||
protected IDBConnection $dbConn,
|
||||
protected IRootFolder $rootFolder,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -46,95 +83,20 @@ class MountProvider implements IMountProvider {
|
||||
* @return IMountPoint[]
|
||||
*/
|
||||
public function getMountsForUser(IUser $user, IStorageFactory $loader) {
|
||||
$userId = $user->getUID();
|
||||
$shares = array_merge(
|
||||
$this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_USER, null, -1),
|
||||
$this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, -1),
|
||||
$this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_CIRCLE, null, -1),
|
||||
$this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_ROOM, null, -1),
|
||||
$this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_DECK, null, -1),
|
||||
$this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_SCIENCEMESH, null, -1),
|
||||
$this->shareManager->getSharedWith($userId, IShare::TYPE_USER, null, -1),
|
||||
$this->shareManager->getSharedWith($userId, IShare::TYPE_GROUP, null, -1),
|
||||
$this->shareManager->getSharedWith($userId, IShare::TYPE_CIRCLE, null, -1),
|
||||
$this->shareManager->getSharedWith($userId, IShare::TYPE_ROOM, null, -1),
|
||||
$this->shareManager->getSharedWith($userId, IShare::TYPE_DECK, null, -1),
|
||||
$this->shareManager->getSharedWith($userId, IShare::TYPE_SCIENCEMESH, null, -1),
|
||||
);
|
||||
|
||||
// filter out excluded shares and group shares that includes self
|
||||
$shares = array_filter($shares, function (IShare $share) use ($user) {
|
||||
return $share->getPermissions() > 0 && $share->getShareOwner() !== $user->getUID() && $share->getSharedBy() !== $user->getUID();
|
||||
});
|
||||
|
||||
$shares = $this->filterShares($shares, $userId);
|
||||
$superShares = $this->buildSuperShares($shares, $user);
|
||||
|
||||
$allMounts = $this->mountManager->getAll();
|
||||
$mounts = [];
|
||||
$view = new View('/' . $user->getUID() . '/files');
|
||||
$ownerViews = [];
|
||||
$sharingDisabledForUser = $this->shareManager->sharingDisabledForUser($user->getUID());
|
||||
/** @var CappedMemoryCache<bool> $folderExistCache */
|
||||
$foldersExistCache = new CappedMemoryCache();
|
||||
|
||||
$validShareCache = $this->cacheFactory->createLocal('share-valid-mountpoint-max');
|
||||
$maxValidatedShare = $validShareCache->get($user->getUID()) ?? 0;
|
||||
$newMaxValidatedShare = $maxValidatedShare;
|
||||
|
||||
foreach ($superShares as $share) {
|
||||
try {
|
||||
/** @var IShare $parentShare */
|
||||
$parentShare = $share[0];
|
||||
|
||||
if ($parentShare->getStatus() !== IShare::STATUS_ACCEPTED
|
||||
&& ($parentShare->getShareType() === IShare::TYPE_GROUP
|
||||
|| $parentShare->getShareType() === IShare::TYPE_USERGROUP
|
||||
|| $parentShare->getShareType() === IShare::TYPE_USER)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$owner = $parentShare->getShareOwner();
|
||||
if (!isset($ownerViews[$owner])) {
|
||||
$ownerViews[$owner] = new View('/' . $parentShare->getShareOwner() . '/files');
|
||||
}
|
||||
$shareId = (int)$parentShare->getId();
|
||||
$mount = new SharedMount(
|
||||
'\OCA\Files_Sharing\SharedStorage',
|
||||
$allMounts,
|
||||
[
|
||||
'user' => $user->getUID(),
|
||||
// parent share
|
||||
'superShare' => $parentShare,
|
||||
// children/component of the superShare
|
||||
'groupedShares' => $share[1],
|
||||
'ownerView' => $ownerViews[$owner],
|
||||
'sharingDisabledForUser' => $sharingDisabledForUser
|
||||
],
|
||||
$loader,
|
||||
$view,
|
||||
$foldersExistCache,
|
||||
$this->eventDispatcher,
|
||||
$user,
|
||||
($shareId <= $maxValidatedShare),
|
||||
);
|
||||
|
||||
$newMaxValidatedShare = max($shareId, $newMaxValidatedShare);
|
||||
|
||||
$event = new ShareMountedEvent($mount);
|
||||
$this->eventDispatcher->dispatchTyped($event);
|
||||
|
||||
$mounts[$mount->getMountPoint()] = $allMounts[$mount->getMountPoint()] = $mount;
|
||||
foreach ($event->getAdditionalMounts() as $additionalMount) {
|
||||
$allMounts[$additionalMount->getMountPoint()] = $mounts[$additionalMount->getMountPoint()] = $additionalMount;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error(
|
||||
'Error while trying to create shared mount',
|
||||
[
|
||||
'app' => 'files_sharing',
|
||||
'exception' => $e,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$validShareCache->set($user->getUID(), $newMaxValidatedShare, 24 * 60 * 60);
|
||||
|
||||
// array_filter removes the null values from the array
|
||||
return array_values(array_filter($mounts));
|
||||
return $this->getMountsFromSuperShares($userId, $superShares, $loader, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,10 +110,11 @@ class MountProvider implements IMountProvider {
|
||||
$tmp = [];
|
||||
|
||||
foreach ($shares as $share) {
|
||||
if (!isset($tmp[$share->getNodeId()])) {
|
||||
$tmp[$share->getNodeId()] = [];
|
||||
$nodeId = $share->getNodeId();
|
||||
if (!isset($tmp[$nodeId])) {
|
||||
$tmp[$nodeId] = [];
|
||||
}
|
||||
$tmp[$share->getNodeId()][] = $share;
|
||||
$tmp[$nodeId][] = $share;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
@@ -168,25 +131,26 @@ class MountProvider implements IMountProvider {
|
||||
$result[] = $tmp2;
|
||||
}
|
||||
|
||||
return array_values($result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build super shares (virtual share) by grouping them by node id and target,
|
||||
* then for each group compute the super share and return it along with the matching
|
||||
* grouped shares. The most permissive permissions are used based on the permissions
|
||||
* of all shares within the group.
|
||||
* Groups shares by node ID and builds a new share object (super share)
|
||||
* which represents a summarized version of all the shares in the group.
|
||||
*
|
||||
* The permissions and attributes of the super share are accumulated from
|
||||
* the shares in the group, forming the most permissive combination
|
||||
* possible.
|
||||
*
|
||||
* @param IShare[] $allShares
|
||||
* @param IUser $user user
|
||||
* @return array Tuple of [superShare, groupedShares]
|
||||
* @return list<array{IShare, array<IShare>}> Tuple of [superShare, groupedShares]
|
||||
*/
|
||||
private function buildSuperShares(array $allShares, IUser $user) {
|
||||
$result = [];
|
||||
|
||||
$groupedShares = $this->groupShares($allShares);
|
||||
|
||||
/** @var IShare[] $shares */
|
||||
foreach ($groupedShares as $shares) {
|
||||
if (count($shares) === 0) {
|
||||
continue;
|
||||
@@ -201,14 +165,7 @@ class MountProvider implements IMountProvider {
|
||||
->setShareType($shares[0]->getShareType())
|
||||
->setTarget($shares[0]->getTarget());
|
||||
|
||||
// Gather notes from all the shares.
|
||||
// Since these are readly available here, storing them
|
||||
// enables the DAV FilesPlugin to avoid executing many
|
||||
// DB queries to retrieve the same information.
|
||||
$allNotes = implode("\n", array_map(function ($sh) {
|
||||
return $sh->getNote();
|
||||
}, $shares));
|
||||
$superShare->setNote($allNotes);
|
||||
$this->combineNotes($shares, $superShare);
|
||||
|
||||
// use most permissive permissions
|
||||
// this covers the case where there are multiple shares for the same
|
||||
@@ -217,7 +174,6 @@ class MountProvider implements IMountProvider {
|
||||
$superAttributes = $this->shareManager->newShare()->newAttributes();
|
||||
$status = IShare::STATUS_PENDING;
|
||||
foreach ($shares as $share) {
|
||||
$superPermissions |= $share->getPermissions();
|
||||
$status = max($status, $share->getStatus());
|
||||
// update permissions
|
||||
$superPermissions |= $share->getPermissions();
|
||||
@@ -225,38 +181,11 @@ class MountProvider implements IMountProvider {
|
||||
// update share permission attributes
|
||||
$attributes = $share->getAttributes();
|
||||
if ($attributes !== null) {
|
||||
foreach ($attributes->toArray() as $attribute) {
|
||||
if ($superAttributes->getAttribute($attribute['scope'], $attribute['key']) === true) {
|
||||
// if super share attribute is already enabled, it is most permissive
|
||||
continue;
|
||||
}
|
||||
// update supershare attributes with subshare attribute
|
||||
$superAttributes->setAttribute($attribute['scope'], $attribute['key'], $attribute['value']);
|
||||
}
|
||||
$this->mergeAttributes($attributes, $superAttributes);
|
||||
}
|
||||
|
||||
// adjust target, for database consistency if needed
|
||||
if ($share->getTarget() !== $superShare->getTarget()) {
|
||||
$share->setTarget($superShare->getTarget());
|
||||
try {
|
||||
$this->shareManager->moveShare($share, $user->getUID());
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// ignore as it is not important and we don't want to
|
||||
// block FS setup
|
||||
|
||||
// the subsequent code anyway only uses the target of the
|
||||
// super share
|
||||
|
||||
// such issue can usually happen when dealing with
|
||||
// null groups which usually appear with group backend
|
||||
// caching inconsistencies
|
||||
$this->logger->debug(
|
||||
'Could not adjust share target for share ' . $share->getId() . ' to make it consistent: ' . $e->getMessage(),
|
||||
['app' => 'files_sharing']
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!is_null($share->getNodeCacheEntry())) {
|
||||
$this->adjustTarget($share, $superShare, $user);
|
||||
if ($share->getNodeCacheEntry() !== null) {
|
||||
$superShare->setNodeCacheEntry($share->getNodeCacheEntry());
|
||||
}
|
||||
}
|
||||
@@ -270,4 +199,305 @@ class MountProvider implements IMountProvider {
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines $attributes into the most permissive set of attributes and
|
||||
* sets them in $superAttributes.
|
||||
*/
|
||||
public function mergeAttributes(
|
||||
IAttributes $attributes,
|
||||
IAttributes $superAttributes,
|
||||
): void {
|
||||
foreach ($attributes->toArray() as $attribute) {
|
||||
if ($superAttributes->getAttribute(
|
||||
$attribute['scope'],
|
||||
$attribute['key']
|
||||
) === true) {
|
||||
// if super share attribute is already enabled, it is most permissive
|
||||
continue;
|
||||
}
|
||||
// update super share attributes with subshare attribute
|
||||
$superAttributes->setAttribute(
|
||||
$attribute['scope'],
|
||||
$attribute['key'],
|
||||
$attribute['value']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather notes from all the shares. Since these are readily available
|
||||
* here, storing them enables the DAV FilesPlugin to avoid executing many
|
||||
* DB queries to retrieve the same information.
|
||||
*
|
||||
* @param array<IShare> $shares
|
||||
* @param IShare $superShare
|
||||
* @return void
|
||||
*/
|
||||
public function combineNotes(
|
||||
array &$shares,
|
||||
IShare $superShare,
|
||||
): void {
|
||||
$allNotes = implode(
|
||||
"\n",
|
||||
array_map(static fn ($sh) => $sh->getNote(), $shares)
|
||||
);
|
||||
$superShare->setNote($allNotes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the target in $share for DB consistency, if needed.
|
||||
*/
|
||||
public function adjustTarget(
|
||||
IShare $share,
|
||||
IShare $superShare,
|
||||
IUser $user,
|
||||
): void {
|
||||
if ($share->getTarget() === $superShare->getTarget()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$share->setTarget($superShare->getTarget());
|
||||
try {
|
||||
$this->shareManager->moveShare($share, $user->getUID());
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// ignore as it is not important and we don't want to
|
||||
// block FS setup
|
||||
|
||||
// the subsequent code anyway only uses the target of the
|
||||
// super share
|
||||
|
||||
// such issue can usually happen when dealing with
|
||||
// null groups which usually appear with group backend
|
||||
// caching inconsistencies
|
||||
$this->logger->debug(
|
||||
'Could not adjust share target for share ' . $share->getId(
|
||||
) . ' to make it consistent: ' . $e->getMessage(),
|
||||
['app' => 'files_sharing']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getMountsFromMountPoints(
|
||||
string $path,
|
||||
array $mountProviderArgs,
|
||||
IStorageFactory $loader,
|
||||
): array {
|
||||
if (empty($mountProviderArgs)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$uniqueMountOwnerIds = [];
|
||||
$uniqueRootIds = [];
|
||||
$user = null;
|
||||
foreach ($mountProviderArgs as $mountProviderArg) {
|
||||
$mountInfo = $mountProviderArg->mountInfo;
|
||||
// get a list of unique owner IDs root mount IDs
|
||||
$user ??= $mountInfo->getUser();
|
||||
$uniqueMountOwnerIds[$user->getUID()] ??= true;
|
||||
$uniqueRootIds[$mountInfo->getRootId()] ??= true;
|
||||
}
|
||||
$uniqueMountOwnerIds = array_keys($uniqueMountOwnerIds);
|
||||
$uniqueRootIds = array_keys($uniqueRootIds);
|
||||
|
||||
// make sure the MPs belong to the same user
|
||||
if ($user === null || count($uniqueMountOwnerIds) !== 1) {
|
||||
// question: what kind of exception to throw in here?
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$rootIdsByShareProviderType = $this->getShareInfo($user, $uniqueRootIds);
|
||||
$sharesInPath = [];
|
||||
foreach ($rootIdsByShareProviderType as $shareType => $rootIds) {
|
||||
// todo: pagination for many rootIds
|
||||
$sharesInPath[] = $this->shareManager->getSharedWithByNodes(
|
||||
$uniqueMountOwnerIds[0],
|
||||
$shareType,
|
||||
$rootIds,
|
||||
-1
|
||||
);
|
||||
}
|
||||
$sharesInPath = array_merge(...$sharesInPath);
|
||||
|
||||
// filter out shares owned or shared by the user and ones for which
|
||||
// the user has no permissions
|
||||
$shares = $this->filterShares($sharesInPath, $user->getUID());
|
||||
$superShares = $this->buildSuperShares($shares, $user);
|
||||
|
||||
return $this->getMountsFromSuperShares(
|
||||
$user->getUID(),
|
||||
$superShares,
|
||||
$loader,
|
||||
$user,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $userId
|
||||
* @param array $superShares
|
||||
* @param IStorageFactory $loader
|
||||
* @param IUser $user
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getMountsFromSuperShares(
|
||||
string $userId,
|
||||
array $superShares,
|
||||
IStorageFactory $loader,
|
||||
IUser $user,
|
||||
): array {
|
||||
$allMounts = $this->mountManager->getAll();
|
||||
$mounts = [];
|
||||
$view = new View('/' . $userId . '/files');
|
||||
$ownerViews = [];
|
||||
$sharingDisabledForUser
|
||||
= $this->shareManager->sharingDisabledForUser($userId);
|
||||
/** @var CappedMemoryCache<bool> $folderExistCache */
|
||||
$foldersExistCache = new CappedMemoryCache();
|
||||
|
||||
$validShareCache
|
||||
= $this->cacheFactory->createLocal('share-valid-mountpoint-max');
|
||||
$maxValidatedShare = $validShareCache->get($userId) ?? 0;
|
||||
$newMaxValidatedShare = $maxValidatedShare;
|
||||
|
||||
foreach ($superShares as $share) {
|
||||
[$parentShare, $groupedShares] = $share;
|
||||
try {
|
||||
if ($parentShare->getStatus() !== IShare::STATUS_ACCEPTED
|
||||
&& ($parentShare->getShareType() === IShare::TYPE_GROUP
|
||||
|| $parentShare->getShareType() === IShare::TYPE_USERGROUP
|
||||
|| $parentShare->getShareType() === IShare::TYPE_USER)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$owner = $parentShare->getShareOwner();
|
||||
if (!isset($ownerViews[$owner])) {
|
||||
$ownerViews[$owner] = new View('/' . $owner . '/files');
|
||||
}
|
||||
$shareId = (int)$parentShare->getId();
|
||||
$mount = new SharedMount(
|
||||
'\OCA\Files_Sharing\SharedStorage',
|
||||
$allMounts,
|
||||
[
|
||||
'user' => $userId,
|
||||
// parent share
|
||||
'superShare' => $parentShare,
|
||||
// children/component of the superShare
|
||||
'groupedShares' => $groupedShares,
|
||||
'ownerView' => $ownerViews[$owner],
|
||||
'sharingDisabledForUser' => $sharingDisabledForUser
|
||||
],
|
||||
$loader,
|
||||
$view,
|
||||
$foldersExistCache,
|
||||
$this->eventDispatcher,
|
||||
$user,
|
||||
$shareId <= $maxValidatedShare,
|
||||
);
|
||||
|
||||
$newMaxValidatedShare = max($shareId, $newMaxValidatedShare);
|
||||
|
||||
$event = new ShareMountedEvent($mount);
|
||||
$this->eventDispatcher->dispatchTyped($event);
|
||||
|
||||
$mounts[$mount->getMountPoint()]
|
||||
= $allMounts[$mount->getMountPoint()] = $mount;
|
||||
foreach ($event->getAdditionalMounts() as $additionalMount) {
|
||||
$allMounts[$additionalMount->getMountPoint()]
|
||||
= $mounts[$additionalMount->getMountPoint()]
|
||||
= $additionalMount;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error(
|
||||
'Error while trying to create shared mount',
|
||||
[
|
||||
'app' => 'files_sharing',
|
||||
'exception' => $e,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$validShareCache->set($userId, $newMaxValidatedShare, 24 * 60 * 60);
|
||||
|
||||
// array_filter removes the null values from the array
|
||||
return array_filter($mounts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out shares owned or shared by the user and ones for which the
|
||||
* user has no permissions.
|
||||
*
|
||||
* @param IShare[] $shares
|
||||
* @return IShare[]
|
||||
*/
|
||||
public function filterShares(array $shares, string $userId): array {
|
||||
return array_filter(
|
||||
$shares,
|
||||
static function (IShare $share) use ($userId) {
|
||||
return $share->getPermissions() > 0
|
||||
&& $share->getShareOwner() !== $userId
|
||||
&& $share->getSharedBy() !== $userId;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to retrieve data needed to determine which
|
||||
* IShareProviders need to be queried: IShare::TYPE_*.
|
||||
*
|
||||
* @see IShare::TYPE_* constants
|
||||
*
|
||||
* @param int[] $uniqueRootIds
|
||||
* @return array<int, int[]> Array of the shared node IDs,
|
||||
* keyed by the share type.
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function getShareInfo(IUser $user, array $uniqueRootIds): array {
|
||||
$mountOwnerId = $user->getUID();
|
||||
// retrieve the share type for the received files
|
||||
$qb = $this->dbConn->getQueryBuilder();
|
||||
$qb->select('file_source', 'share_type')
|
||||
->from('share')
|
||||
->where(
|
||||
$qb->expr()->in(
|
||||
'file_source',
|
||||
$qb->createNamedParameter(
|
||||
$uniqueRootIds,
|
||||
IQueryBuilder::PARAM_STR_ARRAY
|
||||
)
|
||||
)
|
||||
)
|
||||
->andWhere(
|
||||
$qb->expr()->in(
|
||||
'share_type',
|
||||
$qb->createNamedParameter(
|
||||
self::SUPPORTED_SHARE_TYPES,
|
||||
IQueryBuilder::PARAM_INT_ARRAY
|
||||
)
|
||||
)
|
||||
)
|
||||
->andWhere(
|
||||
$qb->expr()->eq(
|
||||
'share_with',
|
||||
$qb->createNamedParameter($mountOwnerId)
|
||||
)
|
||||
)
|
||||
->groupBy('file_source', 'share_type');
|
||||
$cursor = $qb->executeQuery();
|
||||
|
||||
// group IDs of the roots of the mountpoints by type
|
||||
$rootIdsByType = [];
|
||||
while ($row = $cursor->fetch()) {
|
||||
$mappedType = self::TYPE_MAPPING[$row['share_type']] ?? $row['share_type'];
|
||||
$rootIdsByType[$mappedType][] = $row['file_source'];
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
|
||||
return $rootIdsByType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -944,6 +944,39 @@ class ShareByMailProvider extends DefaultShareProvider implements IShareProvider
|
||||
return $shares;
|
||||
}
|
||||
|
||||
public function getSharedWithByNodeIds($userId, $shareType, $nodeIds, $limit, $offset): array {
|
||||
/** @var IShare[] $shares */
|
||||
$shares = [];
|
||||
|
||||
//Get shares directly with this user
|
||||
$qb = $this->dbConnection->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('share');
|
||||
|
||||
// Order by id
|
||||
$qb->orderBy('id');
|
||||
|
||||
// Set limit and offset
|
||||
if ($limit !== -1) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
$qb->setFirstResult($offset);
|
||||
|
||||
$qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)));
|
||||
$qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)));
|
||||
$qb->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter ($nodeIds, IQueryBuilder::PARAM_INT_ARRAY)));
|
||||
|
||||
$cursor = $qb->executeQuery();
|
||||
|
||||
while ($data = $cursor->fetch()) {
|
||||
$shares[] = $this->createShareObject($data);
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
|
||||
|
||||
return $shares;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a share by token
|
||||
*
|
||||
|
||||
@@ -1685,16 +1685,13 @@
|
||||
</file>
|
||||
<file src="apps/files_sharing/lib/MountProvider.php">
|
||||
<InternalClass>
|
||||
<code><![CDATA[new View('/' . $parentShare->getShareOwner() . '/files')]]></code>
|
||||
<code><![CDATA[new View('/' . $user->getUID() . '/files')]]></code>
|
||||
<code><![CDATA[new View('/' . $owner . '/files')]]></code>
|
||||
<code><![CDATA[new View('/' . $userId . '/files')]]></code>
|
||||
</InternalClass>
|
||||
<InternalMethod>
|
||||
<code><![CDATA[new View('/' . $parentShare->getShareOwner() . '/files')]]></code>
|
||||
<code><![CDATA[new View('/' . $user->getUID() . '/files')]]></code>
|
||||
<code><![CDATA[new View('/' . $owner . '/files')]]></code>
|
||||
<code><![CDATA[new View('/' . $userId . '/files')]]></code>
|
||||
</InternalMethod>
|
||||
<RedundantFunctionCall>
|
||||
<code><![CDATA[array_values]]></code>
|
||||
</RedundantFunctionCall>
|
||||
</file>
|
||||
<file src="apps/files_sharing/lib/Scanner.php">
|
||||
<DeprecatedInterface>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OC\Core\Migrations;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\Migration\Attributes\AddColumn;
|
||||
use OCP\Migration\Attributes\ColumnType;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
use Override;
|
||||
|
||||
#[AddColumn(table: 'mounts', name: 'parent_id', type: ColumnType::INTEGER, description: 'Add parent id to easily find a mount\'s children')]
|
||||
class Version33000Date20251124143910 extends SimpleMigrationStep {
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure(): ISchemaWrapper $schemaClosure
|
||||
* @param array $options
|
||||
*/
|
||||
#[Override]
|
||||
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure(): ISchemaWrapper $schemaClosure
|
||||
*/
|
||||
#[Override]
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
$schema = $schemaClosure();
|
||||
|
||||
if ($schema->hasTable('mounts')) {
|
||||
$table = $schema->getTable('mounts');
|
||||
$table->addColumn('parent_id', 'integer', [
|
||||
'length' => 4,
|
||||
'notnull' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure(): ISchemaWrapper $schemaClosure
|
||||
*/
|
||||
#[Override]
|
||||
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
|
||||
}
|
||||
}
|
||||
@@ -416,7 +416,9 @@ return array(
|
||||
'OCP\\Files\\Config\\ICachedMountInfo' => $baseDir . '/lib/public/Files/Config/ICachedMountInfo.php',
|
||||
'OCP\\Files\\Config\\IHomeMountProvider' => $baseDir . '/lib/public/Files/Config/IHomeMountProvider.php',
|
||||
'OCP\\Files\\Config\\IMountProvider' => $baseDir . '/lib/public/Files/Config/IMountProvider.php',
|
||||
'OCP\\Files\\Config\\IMountProviderArgs' => $baseDir . '/lib/public/Files/Config/IMountProviderArgs.php',
|
||||
'OCP\\Files\\Config\\IMountProviderCollection' => $baseDir . '/lib/public/Files/Config/IMountProviderCollection.php',
|
||||
'OCP\\Files\\Config\\IPartialMountProvider' => $baseDir . '/lib/public/Files/Config/IPartialMountProvider.php',
|
||||
'OCP\\Files\\Config\\IRootMountProvider' => $baseDir . '/lib/public/Files/Config/IRootMountProvider.php',
|
||||
'OCP\\Files\\Config\\IUserMountCache' => $baseDir . '/lib/public/Files/Config/IUserMountCache.php',
|
||||
'OCP\\Files\\ConnectionLostException' => $baseDir . '/lib/public/Files/ConnectionLostException.php',
|
||||
@@ -1536,6 +1538,7 @@ return array(
|
||||
'OC\\Core\\Migrations\\Version33000Date20251023110529' => $baseDir . '/core/Migrations/Version33000Date20251023110529.php',
|
||||
'OC\\Core\\Migrations\\Version33000Date20251023120529' => $baseDir . '/core/Migrations/Version33000Date20251023120529.php',
|
||||
'OC\\Core\\Migrations\\Version33000Date20251106131209' => $baseDir . '/core/Migrations/Version33000Date20251106131209.php',
|
||||
'OC\\Core\\Migrations\\Version33000Date20251124143910' => $baseDir . '/core/Migrations/Version33000Date20251124143910.php',
|
||||
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
|
||||
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
|
||||
'OC\\Core\\Service\\CronService' => $baseDir . '/core/Service/CronService.php',
|
||||
@@ -1657,6 +1660,7 @@ return array(
|
||||
'OC\\Files\\Cache\\CacheQueryBuilder' => $baseDir . '/lib/private/Files/Cache/CacheQueryBuilder.php',
|
||||
'OC\\Files\\Cache\\FailedCache' => $baseDir . '/lib/private/Files/Cache/FailedCache.php',
|
||||
'OC\\Files\\Cache\\FileAccess' => $baseDir . '/lib/private/Files/Cache/FileAccess.php',
|
||||
'OC\\Files\\Cache\\FileMetadataCache' => $baseDir . '/lib/private/Files/Cache/FileMetadataCache.php',
|
||||
'OC\\Files\\Cache\\HomeCache' => $baseDir . '/lib/private/Files/Cache/HomeCache.php',
|
||||
'OC\\Files\\Cache\\HomePropagator' => $baseDir . '/lib/private/Files/Cache/HomePropagator.php',
|
||||
'OC\\Files\\Cache\\LocalRootScanner' => $baseDir . '/lib/private/Files/Cache/LocalRootScanner.php',
|
||||
|
||||
@@ -457,7 +457,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
||||
'OCP\\Files\\Config\\ICachedMountInfo' => __DIR__ . '/../../..' . '/lib/public/Files/Config/ICachedMountInfo.php',
|
||||
'OCP\\Files\\Config\\IHomeMountProvider' => __DIR__ . '/../../..' . '/lib/public/Files/Config/IHomeMountProvider.php',
|
||||
'OCP\\Files\\Config\\IMountProvider' => __DIR__ . '/../../..' . '/lib/public/Files/Config/IMountProvider.php',
|
||||
'OCP\\Files\\Config\\IMountProviderArgs' => __DIR__ . '/../../..' . '/lib/public/Files/Config/IMountProviderArgs.php',
|
||||
'OCP\\Files\\Config\\IMountProviderCollection' => __DIR__ . '/../../..' . '/lib/public/Files/Config/IMountProviderCollection.php',
|
||||
'OCP\\Files\\Config\\IPartialMountProvider' => __DIR__ . '/../../..' . '/lib/public/Files/Config/IPartialMountProvider.php',
|
||||
'OCP\\Files\\Config\\IRootMountProvider' => __DIR__ . '/../../..' . '/lib/public/Files/Config/IRootMountProvider.php',
|
||||
'OCP\\Files\\Config\\IUserMountCache' => __DIR__ . '/../../..' . '/lib/public/Files/Config/IUserMountCache.php',
|
||||
'OCP\\Files\\ConnectionLostException' => __DIR__ . '/../../..' . '/lib/public/Files/ConnectionLostException.php',
|
||||
@@ -1577,6 +1579,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
||||
'OC\\Core\\Migrations\\Version33000Date20251023110529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251023110529.php',
|
||||
'OC\\Core\\Migrations\\Version33000Date20251023120529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251023120529.php',
|
||||
'OC\\Core\\Migrations\\Version33000Date20251106131209' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251106131209.php',
|
||||
'OC\\Core\\Migrations\\Version33000Date20251124143910' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251124143910.php',
|
||||
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
|
||||
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
|
||||
'OC\\Core\\Service\\CronService' => __DIR__ . '/../../..' . '/core/Service/CronService.php',
|
||||
@@ -1698,6 +1701,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
||||
'OC\\Files\\Cache\\CacheQueryBuilder' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/CacheQueryBuilder.php',
|
||||
'OC\\Files\\Cache\\FailedCache' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/FailedCache.php',
|
||||
'OC\\Files\\Cache\\FileAccess' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/FileAccess.php',
|
||||
'OC\\Files\\Cache\\FileMetadataCache' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/FileMetadataCache.php',
|
||||
'OC\\Files\\Cache\\HomeCache' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/HomeCache.php',
|
||||
'OC\\Files\\Cache\\HomePropagator' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/HomePropagator.php',
|
||||
'OC\\Files\\Cache\\LocalRootScanner' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/LocalRootScanner.php',
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OC\Files\Cache;
|
||||
|
||||
use OCP\Cache\CappedMemoryCache;
|
||||
use OCP\Files\Cache\ICacheEntry;
|
||||
|
||||
/**
|
||||
* This class uses FileAccess to fetch data to populate ICacheEntry objects
|
||||
* and caches them in memory for subsequent access.
|
||||
*/
|
||||
class FileMetadataCache {
|
||||
|
||||
private const CACHE_SIZE = 50000;
|
||||
|
||||
private const MAX_QUERIED_ITEMS = 1000;
|
||||
|
||||
private CappedMemoryCache $fileCache;
|
||||
|
||||
public function __construct(
|
||||
private readonly FileAccess $fileAccess,
|
||||
) {
|
||||
$this->fileCache = new CappedMemoryCache(self::CACHE_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns file metadata by retrieving it from an in-memory cache or the
|
||||
* database.
|
||||
*
|
||||
* @param int[] $fileIds
|
||||
* @return array<int, ICacheEntry|null>
|
||||
* @see FileAccess::getByFileIds()
|
||||
*/
|
||||
public function getByFileIds(array $fileIds): array {
|
||||
// avoid thrashing the cache if we are asked to query too many ids
|
||||
$skipCaching = \count($fileIds) >= self::CACHE_SIZE;
|
||||
|
||||
$cacheArray = $this->fileCache->getData();
|
||||
|
||||
$result = [];
|
||||
$missingIds = [];
|
||||
foreach ($fileIds as $fileId) {
|
||||
$stringFileId = (string)$fileId;
|
||||
if (\array_key_exists($stringFileId, $cacheArray)) {
|
||||
$result[$fileId] = $this->fileCache->get($stringFileId);
|
||||
} else {
|
||||
$missingIds[] = $fileId;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_chunk($missingIds, self::MAX_QUERIED_ITEMS) as $chunk) {
|
||||
foreach ($this->fileAccess->getByFileIds($chunk) as $id => $fileMetadata) {
|
||||
$result[$id] = $fileMetadata;
|
||||
if (!$skipCaching || \count($this->fileCache->getData()) < self::CACHE_SIZE) {
|
||||
$this->fileCache->set((string)$id, $fileMetadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -9,16 +9,21 @@ namespace OC\Files\Config;
|
||||
|
||||
use OC\Hooks\Emitter;
|
||||
use OC\Hooks\EmitterTrait;
|
||||
use OCA\Files_Sharing\MountProvider;
|
||||
use OCP\Diagnostics\IEventLogger;
|
||||
use OCP\Files\Config\IHomeMountProvider;
|
||||
use OCP\Files\Config\IMountProvider;
|
||||
use OCP\Files\Config\IMountProviderArgs;
|
||||
use OCP\Files\Config\IMountProviderCollection;
|
||||
use OCP\Files\Config\IPartialMountProvider;
|
||||
use OCP\Files\Config\IRootMountProvider;
|
||||
use OCP\Files\Config\IUserMountCache;
|
||||
use OCP\Files\Mount\IMountManager;
|
||||
use OCP\Files\Mount\IMountPoint;
|
||||
use OCP\Files\Storage\IStorageFactory;
|
||||
use OCP\IUser;
|
||||
use function get_class;
|
||||
use function in_array;
|
||||
|
||||
class MountProviderCollection implements IMountProviderCollection, Emitter {
|
||||
use EmitterTrait;
|
||||
@@ -29,7 +34,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
|
||||
private array $homeProviders = [];
|
||||
|
||||
/**
|
||||
* @var list<IMountProvider>
|
||||
* @var array<class-string<IMountProvider>, IMountProvider>
|
||||
*/
|
||||
private array $providers = [];
|
||||
|
||||
@@ -67,9 +72,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
|
||||
$mounts = array_map(function (IMountProvider $provider) use ($user, $loader) {
|
||||
return $this->getMountsFromProvider($provider, $user, $loader);
|
||||
}, $providers);
|
||||
$mounts = array_reduce($mounts, function (array $mounts, array $providerMounts) {
|
||||
return array_merge($mounts, $providerMounts);
|
||||
}, []);
|
||||
$mounts = array_merge(...array_values($mounts));
|
||||
return $this->filterMounts($user, $mounts);
|
||||
}
|
||||
|
||||
@@ -77,16 +80,51 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
|
||||
* @return list<IMountPoint>
|
||||
*/
|
||||
public function getMountsForUser(IUser $user): array {
|
||||
return $this->getUserMountsForProviders($user, $this->providers);
|
||||
return $this->getUserMountsForProviders($user, array_values($this->providers));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IMountProviderArgs[] $mountProviderArgs
|
||||
* @return array<string, IMountPoint> IMountPoint array indexed by mount
|
||||
* point.
|
||||
*/
|
||||
public function getUserMountsFromProviderByPath(
|
||||
string $providerClass,
|
||||
string $path,
|
||||
array $mountProviderArgs,
|
||||
): array {
|
||||
$provider = $this->providers[$providerClass] ?? null;
|
||||
if ($provider === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!is_a($providerClass, IPartialMountProvider::class, true)) {
|
||||
throw new \LogicException(
|
||||
'Mount provider does not support partial mounts'
|
||||
);
|
||||
}
|
||||
|
||||
/** @var IPartialMountProvider $provider */
|
||||
return $provider->getMountsFromMountPoints(
|
||||
$path,
|
||||
$mountProviderArgs,
|
||||
$this->loader,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mounts for the user from the specified provider classes.
|
||||
* Providers not registered in the MountProviderCollection will be skipped.
|
||||
*
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return list<IMountPoint>
|
||||
*/
|
||||
public function getUserMountsForProviderClasses(IUser $user, array $mountProviderClasses): array {
|
||||
$providers = array_filter(
|
||||
$this->providers,
|
||||
fn (IMountProvider $mountProvider) => (in_array(get_class($mountProvider), $mountProviderClasses))
|
||||
fn (string $providerClass) => in_array($providerClass, $mountProviderClasses),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
return $this->getUserMountsForProviders($user, $providers);
|
||||
}
|
||||
@@ -99,16 +137,21 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
|
||||
// to check for name collisions
|
||||
$firstMounts = [];
|
||||
if ($providerFilter) {
|
||||
$providers = array_filter($this->providers, $providerFilter);
|
||||
$providers = array_filter($this->providers, $providerFilter, ARRAY_FILTER_USE_KEY);
|
||||
} else {
|
||||
$providers = $this->providers;
|
||||
}
|
||||
$firstProviders = array_filter($providers, function (IMountProvider $provider) {
|
||||
return (get_class($provider) !== 'OCA\Files_Sharing\MountProvider');
|
||||
});
|
||||
$lastProviders = array_filter($providers, function (IMountProvider $provider) {
|
||||
return (get_class($provider) === 'OCA\Files_Sharing\MountProvider');
|
||||
});
|
||||
$firstProviders
|
||||
= array_filter(
|
||||
$providers,
|
||||
fn (string $providerClass) => ($providerClass !== MountProvider::class),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
$lastProviders = array_filter(
|
||||
$providers,
|
||||
fn (string $providerClass) => $providerClass === MountProvider::class,
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
foreach ($firstProviders as $provider) {
|
||||
$mounts = $this->getMountsFromProvider($provider, $user, $this->loader);
|
||||
$firstMounts = array_merge($firstMounts, $mounts);
|
||||
@@ -150,7 +193,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
|
||||
* Add a provider for mount points
|
||||
*/
|
||||
public function registerProvider(IMountProvider $provider): void {
|
||||
$this->providers[] = $provider;
|
||||
$this->providers[get_class($provider)] = $provider;
|
||||
|
||||
$this->emit('\OC\Files\Config', 'registerMountProvider', [$provider]);
|
||||
}
|
||||
@@ -228,7 +271,7 @@ class MountProviderCollection implements IMountProviderCollection, Emitter {
|
||||
* @return list<IMountProvider>
|
||||
*/
|
||||
public function getProviders(): array {
|
||||
return $this->providers;
|
||||
return array_values($this->providers);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,7 @@ use OCP\IDBConnection;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use function array_key_exists;
|
||||
|
||||
/**
|
||||
* Cache mounts points per user in the cache so we can easily look them up
|
||||
@@ -41,6 +42,10 @@ class UserMountCache implements IUserMountCache {
|
||||
private CappedMemoryCache $internalPathCache;
|
||||
/** @var CappedMemoryCache<array> */
|
||||
private CappedMemoryCache $cacheInfoCache;
|
||||
/** @var CappedMemoryCache<array<string, ICachedMountInfo|null>> */
|
||||
private CappedMemoryCache $usersMountsByPath;
|
||||
/** @var CappedMemoryCache<array<string, bool>> */
|
||||
private CappedMemoryCache $userFullyCachedPaths;
|
||||
|
||||
/**
|
||||
* UserMountCache constructor.
|
||||
@@ -55,6 +60,8 @@ class UserMountCache implements IUserMountCache {
|
||||
$this->cacheInfoCache = new CappedMemoryCache();
|
||||
$this->internalPathCache = new CappedMemoryCache();
|
||||
$this->mountsForUsers = new CappedMemoryCache();
|
||||
$this->usersMountsByPath = new CappedMemoryCache();
|
||||
$this->userFullyCachedPaths = new CappedMemoryCache();
|
||||
}
|
||||
|
||||
public function registerMounts(IUser $user, array $mounts, ?array $mountProviderClasses = null) {
|
||||
@@ -236,6 +243,84 @@ class UserMountCache implements IUserMountCache {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a userId and a path, returns the mount information of the best
|
||||
* matching path belonging to the user with the specified userId.
|
||||
*
|
||||
* @param string $userId
|
||||
* @param string $path
|
||||
* @return ICachedMountInfo|null
|
||||
*/
|
||||
private function getLongestMatchingMount(string $userId, string $path): ?ICachedMountInfo {
|
||||
$subPaths = $this->splitInSubPaths($path);
|
||||
$this->userFullyCachedPaths[$userId] ??= [];
|
||||
$isParentFullyCached = array_any(
|
||||
$this->userFullyCachedPaths[$userId],
|
||||
fn (bool $_, string $parentPath) => str_starts_with($path, $parentPath)
|
||||
);
|
||||
|
||||
// is parent fully cached? then no need to compute missing mounts
|
||||
if (!$isParentFullyCached) {
|
||||
$cachedMountInfos = $this->getCachedMountsByPaths($userId, $subPaths);
|
||||
$missingMountPaths = array_diff($subPaths, array_keys($cachedMountInfos));
|
||||
}
|
||||
|
||||
if (!$isParentFullyCached && !empty($missingMountPaths)) {
|
||||
$builder = $this->connection->getQueryBuilder();
|
||||
$query = $builder->select(
|
||||
'storage_id',
|
||||
'root_id',
|
||||
'user_id',
|
||||
'mount_point',
|
||||
'mount_id',
|
||||
'mount_provider_class'
|
||||
)->from('mounts', 'm')
|
||||
->where(
|
||||
$builder->expr()->eq(
|
||||
'user_id',
|
||||
$builder->createNamedParameter($userId
|
||||
),
|
||||
)
|
||||
)->andWhere(
|
||||
$builder->expr()->in(
|
||||
'mount_point',
|
||||
$builder->createNamedParameter(
|
||||
$missingMountPaths,
|
||||
IQueryBuilder::PARAM_STR_ARRAY
|
||||
)
|
||||
)
|
||||
)->orderBy($builder->func()->charLength('mount_point'), 'DESC');
|
||||
|
||||
$result = $query->executeQuery();
|
||||
unset($missingMountPaths);
|
||||
|
||||
$this->usersMountsByPath[$userId] ??= [];
|
||||
$usersMountsByPath = &$this->usersMountsByPath[$userId];
|
||||
while ($mountPointRow = $result->fetch()) {
|
||||
$mount = $this->dbRowToMountInfo(
|
||||
$mountPointRow,
|
||||
[$this, 'getInternalPathForMountInfo']
|
||||
);
|
||||
$usersMountsByPath[$mount->getMountPoint()] = $mount;
|
||||
}
|
||||
$result->closeCursor();
|
||||
|
||||
// cache the info that the sub paths have no mount-point associated
|
||||
foreach ($subPaths as $subPath) {
|
||||
if (!array_key_exists($subPath, $usersMountsByPath)) {
|
||||
$usersMountsByPath[$subPath] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$cachedMountInfos = $this->getCachedMountsByPaths($userId, $subPaths, true);
|
||||
|
||||
// due to the way the cache is built and retrieved, the longest path
|
||||
// is always on top. No need to sort!
|
||||
return reset($cachedMountInfos) ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IUser $user
|
||||
* @return ICachedMountInfo[]
|
||||
@@ -485,6 +570,136 @@ class UserMountCache implements IUserMountCache {
|
||||
$this->mountsForUsers = new CappedMemoryCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits $path in an array of subpaths with a trailing '/'.
|
||||
*
|
||||
* @param string $path
|
||||
* @return array
|
||||
*/
|
||||
private function splitInSubPaths(string $path): array {
|
||||
$subPaths = [];
|
||||
$current = $path;
|
||||
while (true) {
|
||||
// paths always have a trailing slash in mount-points stored in the
|
||||
// oc_mounts table
|
||||
$subPaths[] = rtrim($current, '/') . '/';
|
||||
$current = dirname($current);
|
||||
if ($current === '/' || $current === '.') {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
return $subPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ICachedMountInfo, keyed by path.
|
||||
*
|
||||
* Note that null values are also possible, signalling that the path is not
|
||||
* associated with a mount-point.
|
||||
*
|
||||
* @param string[] $subPaths
|
||||
* @return array<string, ICachedMountInfo|null>
|
||||
*/
|
||||
public function getCachedMountsByPaths(
|
||||
string $userId,
|
||||
array $subPaths,
|
||||
bool $existingOnly = false,
|
||||
): array {
|
||||
$cachedUserPaths = $this->usersMountsByPath->get($userId) ?? [];
|
||||
|
||||
return array_reduce(
|
||||
$subPaths,
|
||||
static function ($carry, $path) use ($existingOnly, $cachedUserPaths) {
|
||||
$hasCache = array_key_exists($path, $cachedUserPaths);
|
||||
if ($hasCache && (!$existingOnly || !empty($cachedUserPaths[$path]))) {
|
||||
$carry[$path] = $cachedUserPaths[$path] ?? null;
|
||||
}
|
||||
return $carry;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getMountsForPath(IUser $user, string $path, bool $includeChildMounts): array {
|
||||
$mount = $this->getLongestMatchingMount($user->getUID(), $path);
|
||||
|
||||
if ($mount === null) {
|
||||
throw new NotFoundException('No mount for path ' . $path);
|
||||
}
|
||||
|
||||
$mounts = [$mount->getMountPoint() => $mount];
|
||||
|
||||
if ($includeChildMounts) {
|
||||
$mounts = array_merge($mounts, $this->getChildMounts($user->getUID(), $path));
|
||||
}
|
||||
|
||||
return $mounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the child-mounts for the provided path.
|
||||
*
|
||||
* @param string $path
|
||||
* @return array
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
private function getChildMounts(string $userId, string $path): array {
|
||||
$path = rtrim($path, '/') . '/';
|
||||
$userCachedPaths = &$this->usersMountsByPath[$userId];
|
||||
$this->userFullyCachedPaths[$userId] ??= [];
|
||||
$userCachedParents = &$this->userFullyCachedPaths[$userId];
|
||||
|
||||
$isParentFullyCached = array_any(
|
||||
$userCachedParents,
|
||||
fn (bool $_, string $parentPath) => str_starts_with(
|
||||
$path,
|
||||
$parentPath
|
||||
)
|
||||
);
|
||||
if (!$isParentFullyCached) {
|
||||
$builder = $this->connection->getQueryBuilder();
|
||||
$query = $builder->select(
|
||||
'storage_id',
|
||||
'root_id',
|
||||
'user_id',
|
||||
'mount_point',
|
||||
'mount_id',
|
||||
'mount_provider_class'
|
||||
)->from('mounts', 'm')->where(
|
||||
$builder->expr()->like(
|
||||
'mount_point',
|
||||
$builder->createNamedParameter($path . '%')
|
||||
)
|
||||
)->andWhere(
|
||||
$builder->expr()->neq(
|
||||
'mount_point',
|
||||
$builder->createNamedParameter($path)
|
||||
)
|
||||
);
|
||||
|
||||
$result = $query->executeQuery();
|
||||
$rows = $result->fetchAll();
|
||||
$result->closeCursor();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$mountInfo = $this->dbRowToMountInfo($row);
|
||||
$userCachedPaths[$mountInfo->getMountPoint()] = $mountInfo;
|
||||
}
|
||||
$userCachedParents[$path] = true;
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
$userCachedPaths,
|
||||
static fn ($cached, $mountPoint) => $mountPoint !== $path
|
||||
&& str_starts_with($mountPoint, $path) && $cached !== null,
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
}
|
||||
|
||||
public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
|
||||
$mounts = $this->getMountsForUser($user);
|
||||
$mountPoints = array_map(function (ICachedMountInfo $mount) {
|
||||
|
||||
@@ -8,6 +8,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace OC\Files;
|
||||
|
||||
use OC\Files\Cache\FileMetadataCache;
|
||||
use OC\Files\Config\MountProviderCollection;
|
||||
use OC\Files\Mount\HomeMountPoint;
|
||||
use OC\Files\Mount\MountPoint;
|
||||
@@ -33,6 +34,8 @@ use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Files\Config\ICachedMountInfo;
|
||||
use OCP\Files\Config\IHomeMountProvider;
|
||||
use OCP\Files\Config\IMountProvider;
|
||||
use OCP\Files\Config\IMountProviderArgs;
|
||||
use OCP\Files\Config\IPartialMountProvider;
|
||||
use OCP\Files\Config\IRootMountProvider;
|
||||
use OCP\Files\Config\IUserMountCache;
|
||||
use OCP\Files\Events\BeforeFileSystemSetupEvent;
|
||||
@@ -53,6 +56,10 @@ use OCP\IUserSession;
|
||||
use OCP\Lockdown\ILockdownManager;
|
||||
use OCP\Share\Events\ShareCreatedEvent;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function in_array;
|
||||
|
||||
class SetupManager {
|
||||
private bool $rootSetup = false;
|
||||
@@ -60,8 +67,18 @@ class SetupManager {
|
||||
private array $setupUsers = [];
|
||||
// List of users for which all mounts are setup
|
||||
private array $setupUsersComplete = [];
|
||||
/** @var array<string, string[]> */
|
||||
/**
|
||||
* An array of provider classes that have been set up, indexed by UserUID.
|
||||
*
|
||||
* @var array<string, class-string<IMountProvider>[]>
|
||||
*/
|
||||
private array $setupUserMountProviders = [];
|
||||
/**
|
||||
* An array of paths that have already been set up
|
||||
*
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private array $setupMountProviderPaths = [];
|
||||
private ICache $cache;
|
||||
private bool $listeningForProviders;
|
||||
private array $fullSetupRequired = [];
|
||||
@@ -82,6 +99,7 @@ class SetupManager {
|
||||
private IConfig $config,
|
||||
private ShareDisableChecker $shareDisableChecker,
|
||||
private IAppManager $appManager,
|
||||
private FileMetadataCache $fileMetadataCache,
|
||||
) {
|
||||
$this->cache = $cacheFactory->createDistributed('setupmanager::');
|
||||
$this->listeningForProviders = false;
|
||||
@@ -98,6 +116,28 @@ class SetupManager {
|
||||
return in_array($user->getUID(), $this->setupUsersComplete, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a path has been cached either directly or through a full setup
|
||||
* of one of its parents.
|
||||
*/
|
||||
private function isPathSetup(string $path, bool $withChildren): bool {
|
||||
// if the exact path was already setup
|
||||
$cacheKey = $path . ':' . $withChildren;
|
||||
if (array_key_exists($cacheKey, $this->setupMountProviderPaths)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// or if any of the ancestors was fully setup
|
||||
while (($path = dirname($path)) !== '/') {
|
||||
$cacheKey = $path . ':1';
|
||||
if (array_key_exists($cacheKey, $this->setupMountProviderPaths)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function setupBuiltinWrappers() {
|
||||
if ($this->setupBuiltinWrappersDone) {
|
||||
return;
|
||||
@@ -195,17 +235,14 @@ class SetupManager {
|
||||
|
||||
$this->eventLogger->start('fs:setup:user:full', 'Setup full filesystem for user');
|
||||
|
||||
if (!isset($this->setupUserMountProviders[$user->getUID()])) {
|
||||
$this->setupUserMountProviders[$user->getUID()] = [];
|
||||
}
|
||||
|
||||
$this->setupUserMountProviders[$user->getUID()] ??= [];
|
||||
$previouslySetupProviders = $this->setupUserMountProviders[$user->getUID()];
|
||||
|
||||
$this->setupForUserWith($user, function () use ($user) {
|
||||
$this->mountProviderCollection->addMountForUser($user, $this->mountManager, function (
|
||||
IMountProvider $provider,
|
||||
string $providerClass,
|
||||
) use ($user) {
|
||||
return !in_array(get_class($provider), $this->setupUserMountProviders[$user->getUID()]);
|
||||
return !in_array($providerClass, $this->setupUserMountProviders[$user->getUID()]);
|
||||
});
|
||||
});
|
||||
$this->afterUserFullySetup($user, $previouslySetupProviders);
|
||||
@@ -213,7 +250,7 @@ class SetupManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* part of the user setup that is run only once per user
|
||||
* Part of the user setup that is run only once per user.
|
||||
*/
|
||||
private function oneTimeUserSetup(IUser $user) {
|
||||
if ($this->isSetupStarted($user)) {
|
||||
@@ -303,11 +340,16 @@ class SetupManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the one-time user setup and, if the user can access the
|
||||
* filesystem, executes $mountCallback.
|
||||
*
|
||||
* @param IUser $user
|
||||
* @param IMountPoint $mounts
|
||||
* @return void
|
||||
* @throws \OCP\HintException
|
||||
* @throws \OC\ServerNotAvailableException
|
||||
* @see self::oneTimeUserSetup()
|
||||
*
|
||||
*/
|
||||
private function setupForUserWith(IUser $user, callable $mountCallback): void {
|
||||
$this->oneTimeUserSetup($user);
|
||||
@@ -373,7 +415,8 @@ class SetupManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the filesystem for the specified path
|
||||
* Set up the filesystem for the specified path, optionally including all
|
||||
* children mounts.
|
||||
*/
|
||||
public function setupForPath(string $path, bool $includeChildren = false): void {
|
||||
$user = $this->getUserForPath($path);
|
||||
@@ -403,6 +446,11 @@ class SetupManager {
|
||||
$setupProviders = &$this->setupUserMountProviders[$user->getUID()];
|
||||
$currentProviders = [];
|
||||
|
||||
// todo: can this be replaced with the new function which returns any
|
||||
// parent mount? it is similar
|
||||
|
||||
// question: would if be safe to use the logic with the LIKE query to
|
||||
// fetch only the mount for the path we are interested into?
|
||||
try {
|
||||
$cachedMount = $this->userMountCache->getMountForPath($user, $path);
|
||||
} catch (NotFoundException $e) {
|
||||
@@ -415,51 +463,138 @@ class SetupManager {
|
||||
$this->eventLogger->start('fs:setup:user:path', "Setup $path filesystem for user");
|
||||
$this->eventLogger->start('fs:setup:user:path:find', "Find mountpoint for $path");
|
||||
|
||||
$mounts = [];
|
||||
if (!in_array($cachedMount->getMountProvider(), $setupProviders)) {
|
||||
$currentProviders[] = $cachedMount->getMountProvider();
|
||||
if ($cachedMount->getMountProvider()) {
|
||||
$setupProviders[] = $cachedMount->getMountProvider();
|
||||
$mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]);
|
||||
} else {
|
||||
$fullProviderMounts = [];
|
||||
$authoritativeMounts = [];
|
||||
|
||||
$mountProvider = $cachedMount->getMountProvider();
|
||||
$mountPoint = $cachedMount->getMountPoint();
|
||||
$isMountProviderSetup = in_array($mountProvider, $setupProviders);
|
||||
// todo: there may be an issue where if a path is setup with children
|
||||
// (only) but this is called with false, then the path is considered
|
||||
// as not cached and that would be wrong (right?!)
|
||||
$isPathSetupAsAuthoritative
|
||||
= $this->isPathSetup(rtrim($mountPoint, '/'), $includeChildren);
|
||||
if (!$isMountProviderSetup && !$isPathSetupAsAuthoritative) {
|
||||
// question: can this be replaced with === '' (+ eventually || === null)?
|
||||
if (!$mountProvider) {
|
||||
$this->logger->debug('mount at ' . $cachedMount->getMountPoint() . ' has no provider set, performing full setup');
|
||||
$this->eventLogger->end('fs:setup:user:path:find');
|
||||
$this->setupForUser($user);
|
||||
$this->eventLogger->end('fs:setup:user:path');
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_a($mountProvider, IPartialMountProvider::class, true)) {
|
||||
$rootId = $cachedMount->getRootId();
|
||||
$rootsMetadata = $this->fileMetadataCache->getByFileIds([$rootId]);
|
||||
$rootMetadata = array_first($rootsMetadata);
|
||||
$providerArgs = new IMountProviderArgs($cachedMount, $rootMetadata);
|
||||
// mark the path as cached (without children for now...)
|
||||
$cacheKey = rtrim($mountPoint, '/') . ':';
|
||||
$this->setupMountProviderPaths[$cacheKey] = true;
|
||||
$authoritativeMounts[] = array_values(
|
||||
$this->mountProviderCollection->getUserMountsFromProviderByPath(
|
||||
$mountProvider,
|
||||
$path,
|
||||
[$providerArgs]
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$currentProviders[] = $mountProvider;
|
||||
$setupProviders[] = $mountProvider;
|
||||
$fullProviderMounts[]
|
||||
= $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$mountProvider]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($includeChildren) {
|
||||
// question: can we replace this with the LIKE query already?
|
||||
$subCachedMounts = $this->userMountCache->getMountsInPath($user, $path);
|
||||
$this->eventLogger->end('fs:setup:user:path:find');
|
||||
|
||||
$needsFullSetup = array_reduce($subCachedMounts, function (bool $needsFullSetup, ICachedMountInfo $cachedMountInfo) {
|
||||
return $needsFullSetup || $cachedMountInfo->getMountProvider() === '';
|
||||
}, false);
|
||||
$needsFullSetup
|
||||
= array_any(
|
||||
$subCachedMounts,
|
||||
fn (ICachedMountInfo $info) => $info->getMountProvider() === ''
|
||||
);
|
||||
|
||||
if ($needsFullSetup) {
|
||||
$this->logger->debug('mount has no provider set, performing full setup');
|
||||
$this->setupForUser($user);
|
||||
$this->eventLogger->end('fs:setup:user:path');
|
||||
return;
|
||||
} else {
|
||||
foreach ($subCachedMounts as $cachedMount) {
|
||||
if (!in_array($cachedMount->getMountProvider(), $setupProviders)) {
|
||||
$currentProviders[] = $cachedMount->getMountProvider();
|
||||
$setupProviders[] = $cachedMount->getMountProvider();
|
||||
$mounts = array_merge($mounts, $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]));
|
||||
}
|
||||
|
||||
$authoritativeCachedMounts = [];
|
||||
foreach ($subCachedMounts as $cachedMount) {
|
||||
/** @var class-string<IMountProvider> $mountProvider */
|
||||
$mountProvider = $cachedMount->getMountProvider();
|
||||
|
||||
// skip setup for already set up providers
|
||||
if (in_array($mountProvider, $setupProviders)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_a($mountProvider, IPartialMountProvider::class, true)) {
|
||||
// skip setup if path was set up as authoritative before
|
||||
if ($this->isPathSetup(rtrim($cachedMount->getMountPoint(), '/'), false)) {
|
||||
continue;
|
||||
}
|
||||
// collect cached mount points for authoritative providers
|
||||
$authoritativeCachedMounts[$mountProvider] ??= [];
|
||||
$authoritativeCachedMounts[$mountProvider][] = $cachedMount;
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentProviders[] = $mountProvider;
|
||||
$setupProviders[] = $mountProvider;
|
||||
$fullProviderMounts[]
|
||||
= $this->mountProviderCollection->getUserMountsForProviderClasses(
|
||||
$user,
|
||||
[$mountProvider]
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($authoritativeCachedMounts)) {
|
||||
$rootIds = array_map(
|
||||
fn (ICachedMountInfo $mount) => $mount->getRootId(),
|
||||
$authoritativeCachedMounts
|
||||
);
|
||||
$rootsMetadata = $this->fileMetadataCache->getByFileIds($rootIds);
|
||||
foreach ($authoritativeCachedMounts as $providerClass
|
||||
=> $cachedMounts) {
|
||||
$providerArgs = array_map(
|
||||
fn (
|
||||
ICachedMountInfo $info,
|
||||
) => $rootsMetadata[$providerClass] ? new
|
||||
IMountProviderArgs(
|
||||
$info, $rootsMetadata[$providerClass]
|
||||
) : null,
|
||||
$cachedMounts
|
||||
);
|
||||
$authoritativeMounts[]
|
||||
= $this->mountProviderCollection->getUserMountsFromProviderByPath(
|
||||
$providerClass,
|
||||
$path,
|
||||
$providerArgs,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->eventLogger->end('fs:setup:user:path:find');
|
||||
}
|
||||
|
||||
if (count($mounts)) {
|
||||
$this->registerMounts($user, $mounts, $currentProviders);
|
||||
$this->setupForUserWith($user, function () use ($mounts) {
|
||||
array_walk($mounts, [$this->mountManager, 'addMount']);
|
||||
$fullProviderMounts = array_merge(...$fullProviderMounts);
|
||||
$authoritativeMounts = array_merge(...$authoritativeMounts);
|
||||
|
||||
if (count($fullProviderMounts) || count($authoritativeMounts)) {
|
||||
if (count($fullProviderMounts)) {
|
||||
$this->registerMounts($user, $fullProviderMounts, $currentProviders);
|
||||
}
|
||||
|
||||
$this->setupForUserWith($user, function () use ($fullProviderMounts, $authoritativeMounts) {
|
||||
$allMounts = [...$fullProviderMounts, ...$authoritativeMounts];
|
||||
array_walk($allMounts, [$this->mountManager, 'addMount']);
|
||||
});
|
||||
} elseif (!$this->isSetupStarted($user)) {
|
||||
$this->oneTimeUserSetup($user);
|
||||
@@ -539,6 +674,7 @@ class SetupManager {
|
||||
$this->setupUsers = [];
|
||||
$this->setupUsersComplete = [];
|
||||
$this->setupUserMountProviders = [];
|
||||
$this->setupMountProviderPaths = [];
|
||||
$this->fullSetupRequired = [];
|
||||
$this->rootSetup = false;
|
||||
$this->mountManager->clear();
|
||||
|
||||
@@ -8,6 +8,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace OC\Files;
|
||||
|
||||
use OC\Files\Cache\FileMetadataCache;
|
||||
use OC\Share20\ShareDisableChecker;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\Diagnostics\IEventLogger;
|
||||
@@ -38,6 +39,7 @@ class SetupManagerFactory {
|
||||
private IConfig $config,
|
||||
private ShareDisableChecker $shareDisableChecker,
|
||||
private IAppManager $appManager,
|
||||
private FileMetadataCache $fileMetadataCache,
|
||||
) {
|
||||
$this->setupManager = null;
|
||||
}
|
||||
@@ -58,6 +60,7 @@ class SetupManagerFactory {
|
||||
$this->config,
|
||||
$this->shareDisableChecker,
|
||||
$this->appManager,
|
||||
$this->fileMetadataCache,
|
||||
);
|
||||
}
|
||||
return $this->setupManager;
|
||||
|
||||
@@ -948,6 +948,120 @@ class DefaultShareProvider implements IShareProviderWithNotification, IShareProv
|
||||
return $shares;
|
||||
}
|
||||
|
||||
public function getSharedWithByNodeIds($userId, $shareType, $nodeIds, $limit, $offset) {
|
||||
/** @var Share[] $shares */
|
||||
$shares = [];
|
||||
|
||||
if ($shareType === IShare::TYPE_USER) {
|
||||
//Get shares directly with this user
|
||||
$qb = $this->dbConn->getQueryBuilder();
|
||||
$qb->select('s.*',
|
||||
'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash',
|
||||
'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime',
|
||||
'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum'
|
||||
)
|
||||
->selectAlias('st.id', 'storage_string_id')
|
||||
->from('share', 's')
|
||||
->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'))
|
||||
->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id'));
|
||||
|
||||
// Order by id
|
||||
$qb->orderBy('s.id');
|
||||
|
||||
// Set limit and offset
|
||||
if ($limit !== -1) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
$qb->setFirstResult($offset);
|
||||
|
||||
$qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USER)))
|
||||
->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)))
|
||||
->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY)))
|
||||
->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($nodeIds, IQueryBuilder::PARAM_INT_ARRAY)));
|
||||
|
||||
$cursor = $qb->executeQuery();
|
||||
|
||||
while ($data = $cursor->fetch()) {
|
||||
if ($data['fileid'] && $data['path'] === null) {
|
||||
$data['path'] = (string)$data['path'];
|
||||
$data['name'] = (string)$data['name'];
|
||||
$data['checksum'] = (string)$data['checksum'];
|
||||
}
|
||||
if ($this->isAccessibleResult($data)) {
|
||||
$shares[] = $this->createShare($data);
|
||||
}
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
} elseif ($shareType === IShare::TYPE_GROUP) {
|
||||
$user = new LazyUser($userId, $this->userManager);
|
||||
$allGroups = $this->groupManager->getUserGroupIds($user);
|
||||
|
||||
/** @var Share[] $shares2 */
|
||||
$shares2 = [];
|
||||
|
||||
$start = 0;
|
||||
while (true) {
|
||||
$groups = array_slice($allGroups, $start, 1000);
|
||||
$start += 1000;
|
||||
|
||||
if ($groups === []) {
|
||||
break;
|
||||
}
|
||||
|
||||
$qb = $this->dbConn->getQueryBuilder();
|
||||
$qb->select('s.*',
|
||||
'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash',
|
||||
'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime',
|
||||
'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum'
|
||||
)
|
||||
->selectAlias('st.id', 'storage_string_id')
|
||||
->from('share', 's')
|
||||
->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'))
|
||||
->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id'))
|
||||
->orderBy('s.id')
|
||||
->setFirstResult(0);
|
||||
|
||||
if ($limit !== -1) {
|
||||
$qb->setMaxResults($limit - count($shares));
|
||||
}
|
||||
|
||||
$groups = array_filter($groups);
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP)))
|
||||
->andWhere($qb->expr()->in('share_with', $qb->createNamedParameter(
|
||||
$groups,
|
||||
IQueryBuilder::PARAM_STR_ARRAY
|
||||
)))
|
||||
->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY)))
|
||||
->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($nodeIds, IQueryBuilder::PARAM_INT_ARRAY)));
|
||||
|
||||
$cursor = $qb->executeQuery();
|
||||
while ($data = $cursor->fetch()) {
|
||||
if ($offset > 0) {
|
||||
$offset--;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->isAccessibleResult($data)) {
|
||||
$share = $this->createShare($data);
|
||||
$shares2[$share->getId()] = $share;
|
||||
}
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
}
|
||||
|
||||
/*
|
||||
* Resolve all group shares to user specific shares
|
||||
*/
|
||||
$shares = $this->resolveGroupShares($shares2, $userId);
|
||||
} else {
|
||||
throw new BackendError('Invalid backend');
|
||||
}
|
||||
|
||||
|
||||
return $shares;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a share by token
|
||||
*
|
||||
|
||||
@@ -1341,6 +1341,39 @@ class Manager implements IManager {
|
||||
return $shares;
|
||||
}
|
||||
|
||||
public function getSharedWithByNodes(
|
||||
$userId,
|
||||
$shareType,
|
||||
$nodeIds,
|
||||
$limit = 50,
|
||||
$offset = 0,
|
||||
) {
|
||||
try {
|
||||
$provider = $this->factory->getProviderForType($shareType);
|
||||
} catch (ProviderException $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$provider instanceof IShareProvider) { // should be
|
||||
// IShareProviderSupportsByNode
|
||||
// load all shares for the user and filter them by node id
|
||||
return [];
|
||||
}
|
||||
|
||||
$shares = $provider->getSharedWithByNodeIds($userId, $shareType, $nodeIds, $limit, $offset);
|
||||
|
||||
// remove all shares which are already expired
|
||||
foreach ($shares as $key => $share) {
|
||||
try {
|
||||
$this->checkShare($share);
|
||||
} catch (ShareNotFound $e) {
|
||||
unset($shares[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $shares;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCP\Files\Config;
|
||||
|
||||
use OCP\Files\Cache\ICacheEntry;
|
||||
|
||||
class IMountProviderArgs {
|
||||
public function __construct(
|
||||
public ICachedMountInfo $mountInfo,
|
||||
public ICacheEntry $cacheEntry,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCP\Files\Config;
|
||||
|
||||
use OCP\Files\Mount\IMountPoint;
|
||||
use OCP\Files\Storage\IStorageFactory;
|
||||
|
||||
/**
|
||||
* This interface represents mounts that can return IMountPoint instances that
|
||||
* are related to the provided mount information and metadata.
|
||||
*/
|
||||
interface IPartialMountProvider extends IMountProvider {
|
||||
|
||||
/**
|
||||
* Given the path for which mounts need to be set up, and an array of
|
||||
* IMountProviderArgs that provide information for the mount point and the
|
||||
* root of the mount, implementations of this function should return
|
||||
* IMountPoint instances after validating that the provided information
|
||||
* is still accurate.
|
||||
*
|
||||
* @param string $path path for which the mounts are setup
|
||||
* @param IMountProviderArgs[] $mountProviderArgs
|
||||
* @param IStorageFactory $loader
|
||||
* @return array<string, IMountPoint> IMountPoint instances, indexed by
|
||||
* mount-point
|
||||
*/
|
||||
public function getMountsFromMountPoints(
|
||||
string $path,
|
||||
array $mountProviderArgs,
|
||||
IStorageFactory $loader,
|
||||
): array;
|
||||
}
|
||||
@@ -132,4 +132,15 @@ interface IUserMountCache {
|
||||
* @since 24.0.0
|
||||
*/
|
||||
public function getMountsInPath(IUser $user, string $path): array;
|
||||
|
||||
/**
|
||||
* Gets the mount information associated with a path and a user.
|
||||
*
|
||||
* @param IUser $user
|
||||
* @param string $path
|
||||
* @param bool $includeChildMounts if true, will also return the mount
|
||||
* information of the direct children of the mount responsible for the path.
|
||||
* @return array<string, ICachedMountInfo> a list of mounts, indexed by mount-point
|
||||
*/
|
||||
public function getMountsForPath(IUser $user, string $path, bool $includeChildMounts): array;
|
||||
}
|
||||
|
||||
@@ -149,6 +149,14 @@ interface IManager {
|
||||
*/
|
||||
public function getSharedWith($userId, $shareType, $node = null, $limit = 50, $offset = 0);
|
||||
|
||||
/**
|
||||
* Get shares shared with $user, filtering by $nodeIds
|
||||
*
|
||||
* @return IShare[]
|
||||
* @since 33.0.0
|
||||
*/
|
||||
public function getSharedWithByNodes($userId, $shareType, $nodeIds, $limit = 50, $offset = 0);
|
||||
|
||||
/**
|
||||
* Get deleted shares shared with $user.
|
||||
* Filter by $node if provided
|
||||
|
||||
@@ -147,6 +147,8 @@ interface IShareProvider {
|
||||
*/
|
||||
public function getSharedWith($userId, $shareType, $node, $limit, $offset);
|
||||
|
||||
public function getSharedWithByNodeIds($userId, $shareType, $nodeIds, $limit, $offset);
|
||||
|
||||
/**
|
||||
* Get a share by token
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user