Compare commits

...

22 Commits

Author SHA1 Message Date
Salvatore Martire 2bcf3a0bcc feat(sharing): implement path-based mount provider changes in MountProviders [skip ci]
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-28 17:33:14 +01:00
Salvatore Martire 2477163910 feat: implement support for authoritative mount providers [skip ci]
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-28 17:19:40 +01:00
Salvatore Martire 77611110d6 feat: add parent_id column in oc_mounts
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-27 14:42:00 +01:00
Salvatore Martire 0d48c8cfe3 feat: add IPartialMountProvider to support authoritative mounts
IMountProviders implementing this interface will be able to take
advantage of authoritative mounts.

The function `getMountsFromMountPoints` will receive the path that
the provider is asked to set-up and an array of IMountProviderArgs
providing information regarding the stored mount points and the
file cache data for the related root. The mount provider should verify
the validity of the mounts and return IMountPoints related to them.

Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-27 14:42:00 +01:00
Salvatore Martire b697547afd refactor: simplify code
replace array_reduce + array_merge with array_merge(...)
replace conditional assignment with ??=

Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-27 14:41:54 +01:00
Salvatore Martire 05768e230d docs: update comments
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 10:01:08 +01:00
Salvatore Martire 831e780071 fix: do not fetch child-mount information when recursing upwards [skip ci]
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:59:19 +01:00
Salvatore Martire a1be9cdbe9 fix: restore files_accesscontrol functionality within getNodeForPath
This commit adds a workaround to restore the functionality of apps that
rely on descending the file-tree to check for access permissions, like
files_accesscontrol. The navigation is done navigating the tree upwards,
from the target node, to the root node.

Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:59:18 +01:00
Salvatore Martire c92bfe4339 fixup! fix: add INodeByPath to Directory [wip]
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:59:18 +01:00
Salvatore Martire 18d9d06efe fixup! fixup! fix: add INodeByPath to Directory [wip]
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:59:18 +01:00
Salvatore Martire 778f87aed3 fixup! fix: add INodeByPath to Directory [wip]
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:59:18 +01:00
Salvatore Martire 88d0df82c2 fixup! fix: add INodeByPath to Directory [wip]
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:59:18 +01:00
Salvatore Martire 447b39e3b4 fixup! fix: add INodeByPath to Directory [wip]
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:59:18 +01:00
Salvatore Martire a66bf500ec fixup! fix: add INodeByPath to Directory [wip]
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:59:18 +01:00
Salvatore Martire 9e911c7973 fix: add INodeByPath to Directory [wip]
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:59:18 +01:00
Salvatore Martire 85d8df549b chore: fix psalm baseline
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:57:36 +01:00
Salvatore Martire 0e9c3905a0 refactor(files_sharing): avoid magic numbers in external/MountProvider
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:57:36 +01:00
Salvatore Martire ba2df3f7c1 refactor(files_sharing): extract getMountsForUser logic
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:57:36 +01:00
Salvatore Martire f66d375c95 refactor(files_sharing): apply DRY in MountProvider
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:57:36 +01:00
Salvatore Martire 05c2491d7f refactor(files_sharing): reduce complexity in MountProvider::buildSuperShares
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:57:36 +01:00
Salvatore Martire 784faa4d71 fix(files_sharing): remove unnecessary array_values in MountProvider
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:57:36 +01:00
Salvatore Martire a0c08267f6 refactor(files_sharing): apply DRY for user and node ID in MountProvider
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2025-11-26 09:57:35 +01:00
22 changed files with 1362 additions and 189 deletions
+62 -1
View File
@@ -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;
}
}
+1 -1
View File
@@ -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
View File
@@ -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;
}
}
+364 -134
View File
@@ -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
*
+4 -7
View File
@@ -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);
}
/**
+215
View File
@@ -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) {
+165 -29
View File
@@ -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
*
+33
View File
@@ -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;
}
+8
View File
@@ -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
+2
View File
@@ -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
*