diff options
Diffstat (limited to 'lib/private/Files/Config')
-rw-r--r-- | lib/private/Files/Config/CachedMountFileInfo.php | 40 | ||||
-rw-r--r-- | lib/private/Files/Config/CachedMountInfo.php | 121 | ||||
-rw-r--r-- | lib/private/Files/Config/LazyPathCachedMountInfo.php | 48 | ||||
-rw-r--r-- | lib/private/Files/Config/LazyStorageMountInfo.php | 84 | ||||
-rw-r--r-- | lib/private/Files/Config/MountProviderCollection.php | 247 | ||||
-rw-r--r-- | lib/private/Files/Config/UserMountCache.php | 502 | ||||
-rw-r--r-- | lib/private/Files/Config/UserMountCacheListener.php | 34 |
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']); + } +} |