aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Files/Config
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Files/Config')
-rw-r--r--lib/private/Files/Config/CachedMountFileInfo.php40
-rw-r--r--lib/private/Files/Config/CachedMountInfo.php121
-rw-r--r--lib/private/Files/Config/LazyPathCachedMountInfo.php48
-rw-r--r--lib/private/Files/Config/LazyStorageMountInfo.php84
-rw-r--r--lib/private/Files/Config/MountProviderCollection.php247
-rw-r--r--lib/private/Files/Config/UserMountCache.php502
-rw-r--r--lib/private/Files/Config/UserMountCacheListener.php34
7 files changed, 1076 insertions, 0 deletions
diff --git a/lib/private/Files/Config/CachedMountFileInfo.php b/lib/private/Files/Config/CachedMountFileInfo.php
new file mode 100644
index 00000000000..69bd4e9301e
--- /dev/null
+++ b/lib/private/Files/Config/CachedMountFileInfo.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Files\Config;
+
+use OCP\Files\Config\ICachedMountFileInfo;
+use OCP\IUser;
+
+class CachedMountFileInfo extends CachedMountInfo implements ICachedMountFileInfo {
+ private string $internalPath;
+
+ public function __construct(
+ IUser $user,
+ int $storageId,
+ int $rootId,
+ string $mountPoint,
+ ?int $mountId,
+ string $mountProvider,
+ string $rootInternalPath,
+ string $internalPath,
+ ) {
+ parent::__construct($user, $storageId, $rootId, $mountPoint, $mountProvider, $mountId, $rootInternalPath);
+ $this->internalPath = $internalPath;
+ }
+
+ public function getInternalPath(): string {
+ if ($this->getRootInternalPath()) {
+ return substr($this->internalPath, strlen($this->getRootInternalPath()) + 1);
+ } else {
+ return $this->internalPath;
+ }
+ }
+
+ public function getPath(): string {
+ return $this->getMountPoint() . $this->getInternalPath();
+ }
+}
diff --git a/lib/private/Files/Config/CachedMountInfo.php b/lib/private/Files/Config/CachedMountInfo.php
new file mode 100644
index 00000000000..79dd6c6ea1d
--- /dev/null
+++ b/lib/private/Files/Config/CachedMountInfo.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Files\Config;
+
+use OC\Files\Filesystem;
+use OCP\Files\Config\ICachedMountInfo;
+use OCP\Files\Node;
+use OCP\IUser;
+
+class CachedMountInfo implements ICachedMountInfo {
+ protected IUser $user;
+ protected int $storageId;
+ protected int $rootId;
+ protected string $mountPoint;
+ protected ?int $mountId;
+ protected string $rootInternalPath;
+ protected string $mountProvider;
+ protected string $key;
+
+ /**
+ * CachedMountInfo constructor.
+ *
+ * @param IUser $user
+ * @param int $storageId
+ * @param int $rootId
+ * @param string $mountPoint
+ * @param int|null $mountId
+ * @param string $rootInternalPath
+ */
+ public function __construct(
+ IUser $user,
+ int $storageId,
+ int $rootId,
+ string $mountPoint,
+ string $mountProvider,
+ ?int $mountId = null,
+ string $rootInternalPath = '',
+ ) {
+ $this->user = $user;
+ $this->storageId = $storageId;
+ $this->rootId = $rootId;
+ $this->mountPoint = $mountPoint;
+ $this->mountId = $mountId;
+ $this->rootInternalPath = $rootInternalPath;
+ if (strlen($mountProvider) > 128) {
+ throw new \Exception("Mount provider $mountProvider name exceeds the limit of 128 characters");
+ }
+ $this->mountProvider = $mountProvider;
+ $this->key = $rootId . '::' . $mountPoint;
+ }
+
+ /**
+ * @return IUser
+ */
+ public function getUser(): IUser {
+ return $this->user;
+ }
+
+ /**
+ * @return int the numeric storage id of the mount
+ */
+ public function getStorageId(): int {
+ return $this->storageId;
+ }
+
+ /**
+ * @return int the fileid of the root of the mount
+ */
+ public function getRootId(): int {
+ return $this->rootId;
+ }
+
+ /**
+ * @return Node|null the root node of the mount
+ */
+ public function getMountPointNode(): ?Node {
+ // TODO injection etc
+ Filesystem::initMountPoints($this->getUser()->getUID());
+ $userNode = \OC::$server->getUserFolder($this->getUser()->getUID());
+ return $userNode->getParent()->getFirstNodeById($this->getRootId());
+ }
+
+ /**
+ * @return string the mount point of the mount for the user
+ */
+ public function getMountPoint(): string {
+ return $this->mountPoint;
+ }
+
+ /**
+ * Get the id of the configured mount
+ *
+ * @return int|null mount id or null if not applicable
+ * @since 9.1.0
+ */
+ public function getMountId(): ?int {
+ return $this->mountId;
+ }
+
+ /**
+ * Get the internal path (within the storage) of the root of the mount
+ *
+ * @return string
+ */
+ public function getRootInternalPath(): string {
+ return $this->rootInternalPath;
+ }
+
+ public function getMountProvider(): string {
+ return $this->mountProvider;
+ }
+
+ public function getKey(): string {
+ return $this->key;
+ }
+}
diff --git a/lib/private/Files/Config/LazyPathCachedMountInfo.php b/lib/private/Files/Config/LazyPathCachedMountInfo.php
new file mode 100644
index 00000000000..d2396109b1a
--- /dev/null
+++ b/lib/private/Files/Config/LazyPathCachedMountInfo.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Files\Config;
+
+use OCP\IUser;
+
+class LazyPathCachedMountInfo extends CachedMountInfo {
+ // we don't allow \ in paths so it makes a great placeholder
+ private const PATH_PLACEHOLDER = '\\PLACEHOLDER\\';
+
+ /** @var callable(CachedMountInfo): string */
+ protected $rootInternalPathCallback;
+
+ /**
+ * @param IUser $user
+ * @param int $storageId
+ * @param int $rootId
+ * @param string $mountPoint
+ * @param string $mountProvider
+ * @param int|null $mountId
+ * @param callable(CachedMountInfo): string $rootInternalPathCallback
+ * @throws \Exception
+ */
+ public function __construct(
+ IUser $user,
+ int $storageId,
+ int $rootId,
+ string $mountPoint,
+ string $mountProvider,
+ ?int $mountId,
+ callable $rootInternalPathCallback,
+ ) {
+ parent::__construct($user, $storageId, $rootId, $mountPoint, $mountProvider, $mountId, self::PATH_PLACEHOLDER);
+ $this->rootInternalPathCallback = $rootInternalPathCallback;
+ }
+
+ public function getRootInternalPath(): string {
+ if ($this->rootInternalPath === self::PATH_PLACEHOLDER) {
+ $this->rootInternalPath = ($this->rootInternalPathCallback)($this);
+ }
+ return $this->rootInternalPath;
+ }
+}
diff --git a/lib/private/Files/Config/LazyStorageMountInfo.php b/lib/private/Files/Config/LazyStorageMountInfo.php
new file mode 100644
index 00000000000..eb2c60dfa46
--- /dev/null
+++ b/lib/private/Files/Config/LazyStorageMountInfo.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Files\Config;
+
+use OCP\Files\Mount\IMountPoint;
+use OCP\IUser;
+
+class LazyStorageMountInfo extends CachedMountInfo {
+ private IMountPoint $mount;
+
+ /**
+ * CachedMountInfo constructor.
+ *
+ * @param IUser $user
+ * @param IMountPoint $mount
+ */
+ public function __construct(IUser $user, IMountPoint $mount) {
+ $this->user = $user;
+ $this->mount = $mount;
+ $this->rootId = 0;
+ $this->storageId = 0;
+ $this->mountPoint = '';
+ $this->key = '';
+ }
+
+ /**
+ * @return int the numeric storage id of the mount
+ */
+ public function getStorageId(): int {
+ if (!$this->storageId) {
+ $this->storageId = $this->mount->getNumericStorageId();
+ }
+ return parent::getStorageId();
+ }
+
+ /**
+ * @return int the fileid of the root of the mount
+ */
+ public function getRootId(): int {
+ if (!$this->rootId) {
+ $this->rootId = $this->mount->getStorageRootId();
+ }
+ return parent::getRootId();
+ }
+
+ /**
+ * @return string the mount point of the mount for the user
+ */
+ public function getMountPoint(): string {
+ if (!$this->mountPoint) {
+ $this->mountPoint = $this->mount->getMountPoint();
+ }
+ return parent::getMountPoint();
+ }
+
+ public function getMountId(): ?int {
+ return $this->mount->getMountId();
+ }
+
+ /**
+ * Get the internal path (within the storage) of the root of the mount
+ *
+ * @return string
+ */
+ public function getRootInternalPath(): string {
+ return $this->mount->getInternalPath($this->mount->getMountPoint());
+ }
+
+ public function getMountProvider(): string {
+ return $this->mount->getMountProvider();
+ }
+
+ public function getKey(): string {
+ if (!$this->key) {
+ $this->key = $this->getRootId() . '::' . $this->getMountPoint();
+ }
+ return $this->key;
+ }
+}
diff --git a/lib/private/Files/Config/MountProviderCollection.php b/lib/private/Files/Config/MountProviderCollection.php
new file mode 100644
index 00000000000..9d63184e05f
--- /dev/null
+++ b/lib/private/Files/Config/MountProviderCollection.php
@@ -0,0 +1,247 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Files\Config;
+
+use OC\Hooks\Emitter;
+use OC\Hooks\EmitterTrait;
+use OCP\Diagnostics\IEventLogger;
+use OCP\Files\Config\IHomeMountProvider;
+use OCP\Files\Config\IMountProvider;
+use OCP\Files\Config\IMountProviderCollection;
+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;
+
+class MountProviderCollection implements IMountProviderCollection, Emitter {
+ use EmitterTrait;
+
+ /**
+ * @var list<IHomeMountProvider>
+ */
+ private array $homeProviders = [];
+
+ /**
+ * @var list<IMountProvider>
+ */
+ private array $providers = [];
+
+ /** @var list<IRootMountProvider> */
+ private array $rootProviders = [];
+
+ /** @var list<callable> */
+ private array $mountFilters = [];
+
+ public function __construct(
+ private IStorageFactory $loader,
+ private IUserMountCache $mountCache,
+ private IEventLogger $eventLogger,
+ ) {
+ }
+
+ /**
+ * @return list<IMountPoint>
+ */
+ private function getMountsFromProvider(IMountProvider $provider, IUser $user, IStorageFactory $loader): array {
+ $class = str_replace('\\', '_', get_class($provider));
+ $uid = $user->getUID();
+ $this->eventLogger->start('fs:setup:provider:' . $class, "Getting mounts from $class for $uid");
+ $mounts = $provider->getMountsForUser($user, $loader) ?? [];
+ $this->eventLogger->end('fs:setup:provider:' . $class);
+ return array_values($mounts);
+ }
+
+ /**
+ * @param list<IMountProvider> $providers
+ * @return list<IMountPoint>
+ */
+ private function getUserMountsForProviders(IUser $user, array $providers): array {
+ $loader = $this->loader;
+ $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);
+ }, []);
+ return $this->filterMounts($user, $mounts);
+ }
+
+ /**
+ * @return list<IMountPoint>
+ */
+ public function getMountsForUser(IUser $user): array {
+ return $this->getUserMountsForProviders($user, $this->providers);
+ }
+
+ /**
+ * @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))
+ );
+ return $this->getUserMountsForProviders($user, $providers);
+ }
+
+ /**
+ * @return list<IMountPoint>
+ */
+ public function addMountForUser(IUser $user, IMountManager $mountManager, ?callable $providerFilter = null): array {
+ // shared mount provider gets to go last since it needs to know existing files
+ // to check for name collisions
+ $firstMounts = [];
+ if ($providerFilter) {
+ $providers = array_filter($this->providers, $providerFilter);
+ } 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');
+ });
+ foreach ($firstProviders as $provider) {
+ $mounts = $this->getMountsFromProvider($provider, $user, $this->loader);
+ $firstMounts = array_merge($firstMounts, $mounts);
+ }
+ $firstMounts = $this->filterMounts($user, $firstMounts);
+ array_walk($firstMounts, [$mountManager, 'addMount']);
+
+ $lateMounts = [];
+ foreach ($lastProviders as $provider) {
+ $mounts = $this->getMountsFromProvider($provider, $user, $this->loader);
+ $lateMounts = array_merge($lateMounts, $mounts);
+ }
+
+ $lateMounts = $this->filterMounts($user, $lateMounts);
+ $this->eventLogger->start('fs:setup:add-mounts', 'Add mounts to the filesystem');
+ array_walk($lateMounts, [$mountManager, 'addMount']);
+ $this->eventLogger->end('fs:setup:add-mounts');
+
+ return array_values(array_merge($lateMounts, $firstMounts));
+ }
+
+ /**
+ * Get the configured home mount for this user
+ *
+ * @since 9.1.0
+ */
+ public function getHomeMountForUser(IUser $user): IMountPoint {
+ $providers = array_reverse($this->homeProviders); // call the latest registered provider first to give apps an opportunity to overwrite builtin
+ foreach ($providers as $homeProvider) {
+ if ($mount = $homeProvider->getHomeMountForUser($user, $this->loader)) {
+ $mount->setMountPoint('/' . $user->getUID()); //make sure the mountpoint is what we expect
+ return $mount;
+ }
+ }
+ throw new \Exception('No home storage configured for user ' . $user);
+ }
+
+ /**
+ * Add a provider for mount points
+ */
+ public function registerProvider(IMountProvider $provider): void {
+ $this->providers[] = $provider;
+
+ $this->emit('\OC\Files\Config', 'registerMountProvider', [$provider]);
+ }
+
+ public function registerMountFilter(callable $filter): void {
+ $this->mountFilters[] = $filter;
+ }
+
+ /**
+ * @param list<IMountPoint> $mountPoints
+ * @return list<IMountPoint>
+ */
+ private function filterMounts(IUser $user, array $mountPoints): array {
+ return array_values(array_filter($mountPoints, function (IMountPoint $mountPoint) use ($user) {
+ foreach ($this->mountFilters as $filter) {
+ if ($filter($mountPoint, $user) === false) {
+ return false;
+ }
+ }
+ return true;
+ }));
+ }
+
+ /**
+ * Add a provider for home mount points
+ *
+ * @param IHomeMountProvider $provider
+ * @since 9.1.0
+ */
+ public function registerHomeProvider(IHomeMountProvider $provider) {
+ $this->homeProviders[] = $provider;
+ $this->emit('\OC\Files\Config', 'registerHomeMountProvider', [$provider]);
+ }
+
+ /**
+ * Get the mount cache which can be used to search for mounts without setting up the filesystem
+ */
+ public function getMountCache(): IUserMountCache {
+ return $this->mountCache;
+ }
+
+ public function registerRootProvider(IRootMountProvider $provider): void {
+ $this->rootProviders[] = $provider;
+ }
+
+ /**
+ * Get all root mountpoints
+ *
+ * @return list<IMountPoint>
+ * @since 20.0.0
+ */
+ public function getRootMounts(): array {
+ $loader = $this->loader;
+ $mounts = array_map(function (IRootMountProvider $provider) use ($loader) {
+ return $provider->getRootMounts($loader);
+ }, $this->rootProviders);
+ $mounts = array_reduce($mounts, function (array $mounts, array $providerMounts) {
+ return array_merge($mounts, $providerMounts);
+ }, []);
+
+ if (count($mounts) === 0) {
+ throw new \Exception('No root mounts provided by any provider');
+ }
+
+ return array_values($mounts);
+ }
+
+ public function clearProviders(): void {
+ $this->providers = [];
+ $this->homeProviders = [];
+ $this->rootProviders = [];
+ }
+
+ /**
+ * @return list<IMountProvider>
+ */
+ public function getProviders(): array {
+ return $this->providers;
+ }
+
+ /**
+ * @return list<IHomeMountProvider>
+ */
+ public function getHomeProviders(): array {
+ return $this->homeProviders;
+ }
+
+ /**
+ * @return list<IRootMountProvider>
+ */
+ public function getRootProviders(): array {
+ return $this->rootProviders;
+ }
+}
diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php
new file mode 100644
index 00000000000..3e53a67a044
--- /dev/null
+++ b/lib/private/Files/Config/UserMountCache.php
@@ -0,0 +1,502 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Files\Config;
+
+use OC\User\LazyUser;
+use OCP\Cache\CappedMemoryCache;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\Diagnostics\IEventLogger;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Config\Event\UserMountAddedEvent;
+use OCP\Files\Config\Event\UserMountRemovedEvent;
+use OCP\Files\Config\Event\UserMountUpdatedEvent;
+use OCP\Files\Config\ICachedMountFileInfo;
+use OCP\Files\Config\ICachedMountInfo;
+use OCP\Files\Config\IUserMountCache;
+use OCP\Files\NotFoundException;
+use OCP\IDBConnection;
+use OCP\IUser;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Cache mounts points per user in the cache so we can easily look them up
+ */
+class UserMountCache implements IUserMountCache {
+
+ /**
+ * Cached mount info.
+ * @var CappedMemoryCache<ICachedMountInfo[]>
+ **/
+ private CappedMemoryCache $mountsForUsers;
+ /**
+ * fileid => internal path mapping for cached mount info.
+ * @var CappedMemoryCache<string>
+ **/
+ private CappedMemoryCache $internalPathCache;
+ /** @var CappedMemoryCache<array> */
+ private CappedMemoryCache $cacheInfoCache;
+
+ /**
+ * UserMountCache constructor.
+ */
+ public function __construct(
+ private IDBConnection $connection,
+ private IUserManager $userManager,
+ private LoggerInterface $logger,
+ private IEventLogger $eventLogger,
+ private IEventDispatcher $eventDispatcher,
+ ) {
+ $this->cacheInfoCache = new CappedMemoryCache();
+ $this->internalPathCache = new CappedMemoryCache();
+ $this->mountsForUsers = new CappedMemoryCache();
+ }
+
+ public function registerMounts(IUser $user, array $mounts, ?array $mountProviderClasses = null) {
+ $this->eventLogger->start('fs:setup:user:register', 'Registering mounts for user');
+ /** @var array<string, ICachedMountInfo> $newMounts */
+ $newMounts = [];
+ foreach ($mounts as $mount) {
+ // filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
+ if ($mount->getStorageRootId() !== -1) {
+ $mountInfo = new LazyStorageMountInfo($user, $mount);
+ $newMounts[$mountInfo->getKey()] = $mountInfo;
+ }
+ }
+
+ $cachedMounts = $this->getMountsForUser($user);
+ if (is_array($mountProviderClasses)) {
+ $cachedMounts = array_filter($cachedMounts, function (ICachedMountInfo $mountInfo) use ($mountProviderClasses, $newMounts) {
+ // for existing mounts that didn't have a mount provider set
+ // we still want the ones that map to new mounts
+ if ($mountInfo->getMountProvider() === '' && isset($newMounts[$mountInfo->getKey()])) {
+ return true;
+ }
+ return in_array($mountInfo->getMountProvider(), $mountProviderClasses);
+ });
+ }
+
+ $addedMounts = [];
+ $removedMounts = [];
+
+ foreach ($newMounts as $mountKey => $newMount) {
+ if (!isset($cachedMounts[$mountKey])) {
+ $addedMounts[] = $newMount;
+ }
+ }
+
+ foreach ($cachedMounts as $mountKey => $cachedMount) {
+ if (!isset($newMounts[$mountKey])) {
+ $removedMounts[] = $cachedMount;
+ }
+ }
+
+ $changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
+
+ if ($addedMounts || $removedMounts || $changedMounts) {
+ $this->connection->beginTransaction();
+ $userUID = $user->getUID();
+ try {
+ foreach ($addedMounts as $mount) {
+ $this->logger->debug("Adding mount '{$mount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $mount->getMountProvider()]);
+ $this->addToCache($mount);
+ /** @psalm-suppress InvalidArgument */
+ $this->mountsForUsers[$userUID][$mount->getKey()] = $mount;
+ }
+ foreach ($removedMounts as $mount) {
+ $this->logger->debug("Removing mount '{$mount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $mount->getMountProvider()]);
+ $this->removeFromCache($mount);
+ unset($this->mountsForUsers[$userUID][$mount->getKey()]);
+ }
+ foreach ($changedMounts as $mountPair) {
+ $newMount = $mountPair[1];
+ $this->logger->debug("Updating mount '{$newMount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $newMount->getMountProvider()]);
+ $this->updateCachedMount($newMount);
+ /** @psalm-suppress InvalidArgument */
+ $this->mountsForUsers[$userUID][$newMount->getKey()] = $newMount;
+ }
+ $this->connection->commit();
+ } catch (\Throwable $e) {
+ $this->connection->rollBack();
+ throw $e;
+ }
+
+ // Only fire events after all mounts have already been adjusted in the database.
+ foreach ($addedMounts as $mount) {
+ $this->eventDispatcher->dispatchTyped(new UserMountAddedEvent($mount));
+ }
+ foreach ($removedMounts as $mount) {
+ $this->eventDispatcher->dispatchTyped(new UserMountRemovedEvent($mount));
+ }
+ foreach ($changedMounts as $mountPair) {
+ $this->eventDispatcher->dispatchTyped(new UserMountUpdatedEvent($mountPair[0], $mountPair[1]));
+ }
+ }
+ $this->eventLogger->end('fs:setup:user:register');
+ }
+
+ /**
+ * @param array<string, ICachedMountInfo> $newMounts
+ * @param array<string, ICachedMountInfo> $cachedMounts
+ * @return list<list{0: ICachedMountInfo, 1: ICachedMountInfo}> Pairs of old and new mounts
+ */
+ private function findChangedMounts(array $newMounts, array $cachedMounts): array {
+ $changed = [];
+ foreach ($cachedMounts as $key => $cachedMount) {
+ if (isset($newMounts[$key])) {
+ $newMount = $newMounts[$key];
+ if (
+ $newMount->getStorageId() !== $cachedMount->getStorageId()
+ || $newMount->getMountId() !== $cachedMount->getMountId()
+ || $newMount->getMountProvider() !== $cachedMount->getMountProvider()
+ ) {
+ $changed[] = [$cachedMount, $newMount];
+ }
+ }
+ }
+ return $changed;
+ }
+
+ private function addToCache(ICachedMountInfo $mount) {
+ if ($mount->getStorageId() !== -1) {
+ $this->connection->insertIfNotExist('*PREFIX*mounts', [
+ 'storage_id' => $mount->getStorageId(),
+ 'root_id' => $mount->getRootId(),
+ 'user_id' => $mount->getUser()->getUID(),
+ 'mount_point' => $mount->getMountPoint(),
+ 'mount_id' => $mount->getMountId(),
+ 'mount_provider_class' => $mount->getMountProvider(),
+ ], ['root_id', 'user_id', 'mount_point']);
+ } else {
+ // in some cases this is legitimate, like orphaned shares
+ $this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
+ }
+ }
+
+ private function updateCachedMount(ICachedMountInfo $mount) {
+ $builder = $this->connection->getQueryBuilder();
+
+ $query = $builder->update('mounts')
+ ->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
+ ->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
+ ->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
+ ->set('mount_provider_class', $builder->createNamedParameter($mount->getMountProvider()))
+ ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
+ ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
+
+ $query->executeStatement();
+ }
+
+ private function removeFromCache(ICachedMountInfo $mount) {
+ $builder = $this->connection->getQueryBuilder();
+
+ $query = $builder->delete('mounts')
+ ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
+ ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)))
+ ->andWhere($builder->expr()->eq('mount_point', $builder->createNamedParameter($mount->getMountPoint())));
+ $query->executeStatement();
+ }
+
+ /**
+ * @param array $row
+ * @param (callable(CachedMountInfo): string)|null $pathCallback
+ * @return CachedMountInfo
+ */
+ private function dbRowToMountInfo(array $row, ?callable $pathCallback = null): ICachedMountInfo {
+ $user = new LazyUser($row['user_id'], $this->userManager);
+ $mount_id = $row['mount_id'];
+ if (!is_null($mount_id)) {
+ $mount_id = (int)$mount_id;
+ }
+ if ($pathCallback) {
+ return new LazyPathCachedMountInfo(
+ $user,
+ (int)$row['storage_id'],
+ (int)$row['root_id'],
+ $row['mount_point'],
+ $row['mount_provider_class'] ?? '',
+ $mount_id,
+ $pathCallback,
+ );
+ } else {
+ return new CachedMountInfo(
+ $user,
+ (int)$row['storage_id'],
+ (int)$row['root_id'],
+ $row['mount_point'],
+ $row['mount_provider_class'] ?? '',
+ $mount_id,
+ $row['path'] ?? '',
+ );
+ }
+ }
+
+ /**
+ * @param IUser $user
+ * @return ICachedMountInfo[]
+ */
+ public function getMountsForUser(IUser $user) {
+ $userUID = $user->getUID();
+ if (!$this->userManager->userExists($userUID)) {
+ return [];
+ }
+ if (!isset($this->mountsForUsers[$userUID])) {
+ $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($userUID)));
+
+ $result = $query->executeQuery();
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+
+ /** @var array<string, ICachedMountInfo> $mounts */
+ $mounts = [];
+ foreach ($rows as $row) {
+ $mount = $this->dbRowToMountInfo($row, [$this, 'getInternalPathForMountInfo']);
+ if ($mount !== null) {
+ $mounts[$mount->getKey()] = $mount;
+ }
+ }
+ $this->mountsForUsers[$userUID] = $mounts;
+ }
+ return $this->mountsForUsers[$userUID];
+ }
+
+ public function getInternalPathForMountInfo(CachedMountInfo $info): string {
+ $cached = $this->internalPathCache->get($info->getRootId());
+ if ($cached !== null) {
+ return $cached;
+ }
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->select('path')
+ ->from('filecache')
+ ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($info->getRootId())));
+ return $query->executeQuery()->fetchOne() ?: '';
+ }
+
+ /**
+ * @param int $numericStorageId
+ * @param string|null $user limit the results to a single user
+ * @return CachedMountInfo[]
+ */
+ public function getMountsForStorageId($numericStorageId, $user = null) {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
+ ->from('mounts', 'm')
+ ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
+ ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
+
+ if ($user) {
+ $query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter($user)));
+ }
+
+ $result = $query->executeQuery();
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+
+ return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
+ }
+
+ /**
+ * @param int $rootFileId
+ * @return CachedMountInfo[]
+ */
+ public function getMountsForRootId($rootFileId) {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
+ ->from('mounts', 'm')
+ ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
+ ->where($builder->expr()->eq('root_id', $builder->createNamedParameter($rootFileId, IQueryBuilder::PARAM_INT)));
+
+ $result = $query->executeQuery();
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+
+ return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
+ }
+
+ /**
+ * @param $fileId
+ * @return array{int, string, int}
+ * @throws \OCP\Files\NotFoundException
+ */
+ private function getCacheInfoFromFileId($fileId): array {
+ if (!isset($this->cacheInfoCache[$fileId])) {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->select('storage', 'path', 'mimetype')
+ ->from('filecache')
+ ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
+
+ $result = $query->executeQuery();
+ $row = $result->fetch();
+ $result->closeCursor();
+
+ if (is_array($row)) {
+ $this->cacheInfoCache[$fileId] = [
+ (int)$row['storage'],
+ (string)$row['path'],
+ (int)$row['mimetype']
+ ];
+ } else {
+ throw new NotFoundException('File with id "' . $fileId . '" not found');
+ }
+ }
+ return $this->cacheInfoCache[$fileId];
+ }
+
+ /**
+ * @param int $fileId
+ * @param string|null $user optionally restrict the results to a single user
+ * @return ICachedMountFileInfo[]
+ * @since 9.0.0
+ */
+ public function getMountsForFileId($fileId, $user = null) {
+ try {
+ [$storageId, $internalPath] = $this->getCacheInfoFromFileId($fileId);
+ } catch (NotFoundException $e) {
+ return [];
+ }
+ $mountsForStorage = $this->getMountsForStorageId($storageId, $user);
+
+ // filter mounts that are from the same storage but not a parent of the file we care about
+ $filteredMounts = array_filter($mountsForStorage, function (ICachedMountInfo $mount) use ($internalPath, $fileId) {
+ if ($fileId === $mount->getRootId()) {
+ return true;
+ }
+ $internalMountPath = $mount->getRootInternalPath();
+
+ return $internalMountPath === '' || str_starts_with($internalPath, $internalMountPath . '/');
+ });
+
+ $filteredMounts = array_values(array_filter($filteredMounts, function (ICachedMountInfo $mount) {
+ return $this->userManager->userExists($mount->getUser()->getUID());
+ }));
+
+ return array_map(function (ICachedMountInfo $mount) use ($internalPath) {
+ return new CachedMountFileInfo(
+ $mount->getUser(),
+ $mount->getStorageId(),
+ $mount->getRootId(),
+ $mount->getMountPoint(),
+ $mount->getMountId(),
+ $mount->getMountProvider(),
+ $mount->getRootInternalPath(),
+ $internalPath
+ );
+ }, $filteredMounts);
+ }
+
+ /**
+ * Remove all cached mounts for a user
+ *
+ * @param IUser $user
+ */
+ public function removeUserMounts(IUser $user) {
+ $builder = $this->connection->getQueryBuilder();
+
+ $query = $builder->delete('mounts')
+ ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
+ $query->executeStatement();
+ }
+
+ public function removeUserStorageMount($storageId, $userId) {
+ $builder = $this->connection->getQueryBuilder();
+
+ $query = $builder->delete('mounts')
+ ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
+ ->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
+ $query->executeStatement();
+ }
+
+ public function remoteStorageMounts($storageId) {
+ $builder = $this->connection->getQueryBuilder();
+
+ $query = $builder->delete('mounts')
+ ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
+ $query->executeStatement();
+ }
+
+ /**
+ * @param array $users
+ * @return array
+ */
+ public function getUsedSpaceForUsers(array $users) {
+ $builder = $this->connection->getQueryBuilder();
+
+ $slash = $builder->createNamedParameter('/');
+
+ $mountPoint = $builder->func()->concat(
+ $builder->func()->concat($slash, 'user_id'),
+ $slash
+ );
+
+ $userIds = array_map(function (IUser $user) {
+ return $user->getUID();
+ }, $users);
+
+ $query = $builder->select('m.user_id', 'f.size')
+ ->from('mounts', 'm')
+ ->innerJoin('m', 'filecache', 'f',
+ $builder->expr()->andX(
+ $builder->expr()->eq('m.storage_id', 'f.storage'),
+ $builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files')))
+ ))
+ ->where($builder->expr()->eq('m.mount_point', $mountPoint))
+ ->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
+
+ $result = $query->executeQuery();
+
+ $results = [];
+ while ($row = $result->fetch()) {
+ $results[$row['user_id']] = $row['size'];
+ }
+ $result->closeCursor();
+ return $results;
+ }
+
+ public function clear(): void {
+ $this->cacheInfoCache = new CappedMemoryCache();
+ $this->mountsForUsers = new CappedMemoryCache();
+ }
+
+ public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
+ $mounts = $this->getMountsForUser($user);
+ $mountPoints = array_map(function (ICachedMountInfo $mount) {
+ return $mount->getMountPoint();
+ }, $mounts);
+ $mounts = array_combine($mountPoints, $mounts);
+
+ $current = rtrim($path, '/');
+ // walk up the directory tree until we find a path that has a mountpoint set
+ // the loop will return if a mountpoint is found or break if none are found
+ while (true) {
+ $mountPoint = $current . '/';
+ if (isset($mounts[$mountPoint])) {
+ return $mounts[$mountPoint];
+ } elseif ($current === '') {
+ break;
+ }
+
+ $current = dirname($current);
+ if ($current === '.' || $current === '/') {
+ $current = '';
+ }
+ }
+
+ throw new NotFoundException('No cached mount for path ' . $path);
+ }
+
+ public function getMountsInPath(IUser $user, string $path): array {
+ $path = rtrim($path, '/') . '/';
+ $mounts = $this->getMountsForUser($user);
+ return array_filter($mounts, function (ICachedMountInfo $mount) use ($path) {
+ return $mount->getMountPoint() !== $path && str_starts_with($mount->getMountPoint(), $path);
+ });
+ }
+}
diff --git a/lib/private/Files/Config/UserMountCacheListener.php b/lib/private/Files/Config/UserMountCacheListener.php
new file mode 100644
index 00000000000..40995de8986
--- /dev/null
+++ b/lib/private/Files/Config/UserMountCacheListener.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Files\Config;
+
+use OC\User\Manager;
+use OCP\Files\Config\IUserMountCache;
+
+/**
+ * Listen to hooks and update the mount cache as needed
+ */
+class UserMountCacheListener {
+ /**
+ * @var IUserMountCache
+ */
+ private $userMountCache;
+
+ /**
+ * UserMountCacheListener constructor.
+ *
+ * @param IUserMountCache $userMountCache
+ */
+ public function __construct(IUserMountCache $userMountCache) {
+ $this->userMountCache = $userMountCache;
+ }
+
+ public function listen(Manager $manager) {
+ $manager->listen('\OC\User', 'postDelete', [$this->userMountCache, 'removeUserMounts']);
+ }
+}