aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Files/SetupManager.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Files/SetupManager.php')
-rw-r--r--lib/private/Files/SetupManager.php609
1 files changed, 609 insertions, 0 deletions
diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php
new file mode 100644
index 00000000000..b92c608a81d
--- /dev/null
+++ b/lib/private/Files/SetupManager.php
@@ -0,0 +1,609 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Files;
+
+use OC\Files\Config\MountProviderCollection;
+use OC\Files\Mount\HomeMountPoint;
+use OC\Files\Mount\MountPoint;
+use OC\Files\Storage\Common;
+use OC\Files\Storage\Home;
+use OC\Files\Storage\Storage;
+use OC\Files\Storage\Wrapper\Availability;
+use OC\Files\Storage\Wrapper\Encoding;
+use OC\Files\Storage\Wrapper\PermissionsMask;
+use OC\Files\Storage\Wrapper\Quota;
+use OC\Lockdown\Filesystem\NullStorage;
+use OC\Share\Share;
+use OC\Share20\ShareDisableChecker;
+use OC_Hook;
+use OCA\Files_External\Config\ExternalMountPoint;
+use OCA\Files_Sharing\External\Mount;
+use OCA\Files_Sharing\ISharedMountPoint;
+use OCA\Files_Sharing\SharedMount;
+use OCP\App\IAppManager;
+use OCP\Constants;
+use OCP\Diagnostics\IEventLogger;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Config\ICachedMountInfo;
+use OCP\Files\Config\IHomeMountProvider;
+use OCP\Files\Config\IMountProvider;
+use OCP\Files\Config\IRootMountProvider;
+use OCP\Files\Config\IUserMountCache;
+use OCP\Files\Events\BeforeFileSystemSetupEvent;
+use OCP\Files\Events\InvalidateMountCacheEvent;
+use OCP\Files\Events\Node\FilesystemTornDownEvent;
+use OCP\Files\Mount\IMountManager;
+use OCP\Files\Mount\IMountPoint;
+use OCP\Files\NotFoundException;
+use OCP\Files\Storage\IStorage;
+use OCP\Group\Events\UserAddedEvent;
+use OCP\Group\Events\UserRemovedEvent;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\Lockdown\ILockdownManager;
+use OCP\Share\Events\ShareCreatedEvent;
+use Psr\Log\LoggerInterface;
+
+class SetupManager {
+ private bool $rootSetup = false;
+ // List of users for which at least one mount is setup
+ private array $setupUsers = [];
+ // List of users for which all mounts are setup
+ private array $setupUsersComplete = [];
+ /** @var array<string, string[]> */
+ private array $setupUserMountProviders = [];
+ private ICache $cache;
+ private bool $listeningForProviders;
+ private array $fullSetupRequired = [];
+ private bool $setupBuiltinWrappersDone = false;
+ private bool $forceFullSetup = false;
+
+ public function __construct(
+ private IEventLogger $eventLogger,
+ private MountProviderCollection $mountProviderCollection,
+ private IMountManager $mountManager,
+ private IUserManager $userManager,
+ private IEventDispatcher $eventDispatcher,
+ private IUserMountCache $userMountCache,
+ private ILockdownManager $lockdownManager,
+ private IUserSession $userSession,
+ ICacheFactory $cacheFactory,
+ private LoggerInterface $logger,
+ private IConfig $config,
+ private ShareDisableChecker $shareDisableChecker,
+ private IAppManager $appManager,
+ ) {
+ $this->cache = $cacheFactory->createDistributed('setupmanager::');
+ $this->listeningForProviders = false;
+ $this->forceFullSetup = $this->config->getSystemValueBool('debug.force-full-fs-setup');
+
+ $this->setupListeners();
+ }
+
+ private function isSetupStarted(IUser $user): bool {
+ return in_array($user->getUID(), $this->setupUsers, true);
+ }
+
+ public function isSetupComplete(IUser $user): bool {
+ return in_array($user->getUID(), $this->setupUsersComplete, true);
+ }
+
+ private function setupBuiltinWrappers() {
+ if ($this->setupBuiltinWrappersDone) {
+ return;
+ }
+ $this->setupBuiltinWrappersDone = true;
+
+ // load all filesystem apps before, so no setup-hook gets lost
+ $this->appManager->loadApps(['filesystem']);
+ $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
+
+ Filesystem::addStorageWrapper('mount_options', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
+ if ($storage->instanceOfStorage(Common::class)) {
+ $options = array_merge($mount->getOptions(), ['mount_point' => $mountPoint]);
+ $storage->setMountOptions($options);
+ }
+ return $storage;
+ });
+
+ $reSharingEnabled = Share::isResharingAllowed();
+ $user = $this->userSession->getUser();
+ $sharingEnabledForUser = $user ? !$this->shareDisableChecker->sharingDisabledForUser($user->getUID()) : true;
+ Filesystem::addStorageWrapper(
+ 'sharing_mask',
+ function ($mountPoint, IStorage $storage, IMountPoint $mount) use ($reSharingEnabled, $sharingEnabledForUser) {
+ $sharingEnabledForMount = $mount->getOption('enable_sharing', true);
+ $isShared = $mount instanceof ISharedMountPoint;
+ if (!$sharingEnabledForMount || !$sharingEnabledForUser || (!$reSharingEnabled && $isShared)) {
+ return new PermissionsMask([
+ 'storage' => $storage,
+ 'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE,
+ ]);
+ }
+ return $storage;
+ }
+ );
+
+ // install storage availability wrapper, before most other wrappers
+ Filesystem::addStorageWrapper('oc_availability', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
+ $externalMount = $mount instanceof ExternalMountPoint || $mount instanceof Mount;
+ if ($externalMount && !$storage->isLocal()) {
+ return new Availability(['storage' => $storage]);
+ }
+ return $storage;
+ });
+
+ Filesystem::addStorageWrapper('oc_encoding', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
+ if ($mount->getOption('encoding_compatibility', false) && !$mount instanceof SharedMount) {
+ return new Encoding(['storage' => $storage]);
+ }
+ return $storage;
+ });
+
+ $quotaIncludeExternal = $this->config->getSystemValue('quota_include_external_storage', false);
+ Filesystem::addStorageWrapper('oc_quota', function ($mountPoint, $storage, IMountPoint $mount) use ($quotaIncludeExternal) {
+ // set up quota for home storages, even for other users
+ // which can happen when using sharing
+ if ($mount instanceof HomeMountPoint) {
+ $user = $mount->getUser();
+ return new Quota(['storage' => $storage, 'quotaCallback' => function () use ($user) {
+ return $user->getQuotaBytes();
+ }, 'root' => 'files', 'include_external_storage' => $quotaIncludeExternal]);
+ }
+
+ return $storage;
+ });
+
+ Filesystem::addStorageWrapper('readonly', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
+ /*
+ * Do not allow any operations that modify the storage
+ */
+ if ($mount->getOption('readonly', false)) {
+ return new PermissionsMask([
+ 'storage' => $storage,
+ 'mask' => Constants::PERMISSION_ALL & ~(
+ Constants::PERMISSION_UPDATE
+ | Constants::PERMISSION_CREATE
+ | Constants::PERMISSION_DELETE
+ ),
+ ]);
+ }
+ return $storage;
+ });
+
+ Filesystem::logWarningWhenAddingStorageWrapper($prevLogging);
+ }
+
+ /**
+ * Setup the full filesystem for the specified user
+ */
+ public function setupForUser(IUser $user): void {
+ if ($this->isSetupComplete($user)) {
+ return;
+ }
+ $this->setupUsersComplete[] = $user->getUID();
+
+ $this->eventLogger->start('fs:setup:user:full', 'Setup full filesystem for user');
+
+ if (!isset($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,
+ ) use ($user) {
+ return !in_array(get_class($provider), $this->setupUserMountProviders[$user->getUID()]);
+ });
+ });
+ $this->afterUserFullySetup($user, $previouslySetupProviders);
+ $this->eventLogger->end('fs:setup:user:full');
+ }
+
+ /**
+ * part of the user setup that is run only once per user
+ */
+ private function oneTimeUserSetup(IUser $user) {
+ if ($this->isSetupStarted($user)) {
+ return;
+ }
+ $this->setupUsers[] = $user->getUID();
+
+ $this->setupRoot();
+
+ $this->eventLogger->start('fs:setup:user:onetime', 'Onetime filesystem for user');
+
+ $this->setupBuiltinWrappers();
+
+ $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
+
+ // TODO remove hook
+ OC_Hook::emit('OC_Filesystem', 'preSetup', ['user' => $user->getUID()]);
+
+ $event = new BeforeFileSystemSetupEvent($user);
+ $this->eventDispatcher->dispatchTyped($event);
+
+ Filesystem::logWarningWhenAddingStorageWrapper($prevLogging);
+
+ $userDir = '/' . $user->getUID() . '/files';
+
+ Filesystem::initInternal($userDir);
+
+ if ($this->lockdownManager->canAccessFilesystem()) {
+ $this->eventLogger->start('fs:setup:user:home', 'Setup home filesystem for user');
+ // home mounts are handled separate since we need to ensure this is mounted before we call the other mount providers
+ $homeMount = $this->mountProviderCollection->getHomeMountForUser($user);
+ $this->mountManager->addMount($homeMount);
+
+ if ($homeMount->getStorageRootId() === -1) {
+ $this->eventLogger->start('fs:setup:user:home:scan', 'Scan home filesystem for user');
+ $homeMount->getStorage()->mkdir('');
+ $homeMount->getStorage()->getScanner()->scan('');
+ $this->eventLogger->end('fs:setup:user:home:scan');
+ }
+ $this->eventLogger->end('fs:setup:user:home');
+ } else {
+ $this->mountManager->addMount(new MountPoint(
+ new NullStorage([]),
+ '/' . $user->getUID()
+ ));
+ $this->mountManager->addMount(new MountPoint(
+ new NullStorage([]),
+ '/' . $user->getUID() . '/files'
+ ));
+ $this->setupUsersComplete[] = $user->getUID();
+ }
+
+ $this->listenForNewMountProviders();
+
+ $this->eventLogger->end('fs:setup:user:onetime');
+ }
+
+ /**
+ * Final housekeeping after a user has been fully setup
+ */
+ private function afterUserFullySetup(IUser $user, array $previouslySetupProviders): void {
+ $this->eventLogger->start('fs:setup:user:full:post', 'Housekeeping after user is setup');
+ $userRoot = '/' . $user->getUID() . '/';
+ $mounts = $this->mountManager->getAll();
+ $mounts = array_filter($mounts, function (IMountPoint $mount) use ($userRoot) {
+ return str_starts_with($mount->getMountPoint(), $userRoot);
+ });
+ $allProviders = array_map(function (IMountProvider|IHomeMountProvider|IRootMountProvider $provider) {
+ return get_class($provider);
+ }, array_merge(
+ $this->mountProviderCollection->getProviders(),
+ $this->mountProviderCollection->getHomeProviders(),
+ $this->mountProviderCollection->getRootProviders(),
+ ));
+ $newProviders = array_diff($allProviders, $previouslySetupProviders);
+ $mounts = array_filter($mounts, function (IMountPoint $mount) use ($previouslySetupProviders) {
+ return !in_array($mount->getMountProvider(), $previouslySetupProviders);
+ });
+ $this->registerMounts($user, $mounts, $newProviders);
+
+ $cacheDuration = $this->config->getSystemValueInt('fs_mount_cache_duration', 5 * 60);
+ if ($cacheDuration > 0) {
+ $this->cache->set($user->getUID(), true, $cacheDuration);
+ $this->fullSetupRequired[$user->getUID()] = false;
+ }
+ $this->eventLogger->end('fs:setup:user:full:post');
+ }
+
+ /**
+ * @param IUser $user
+ * @param IMountPoint $mounts
+ * @return void
+ * @throws \OCP\HintException
+ * @throws \OC\ServerNotAvailableException
+ */
+ private function setupForUserWith(IUser $user, callable $mountCallback): void {
+ $this->oneTimeUserSetup($user);
+
+ if ($this->lockdownManager->canAccessFilesystem()) {
+ $mountCallback();
+ }
+ $this->eventLogger->start('fs:setup:user:post-init-mountpoint', 'post_initMountPoints legacy hook');
+ \OC_Hook::emit('OC_Filesystem', 'post_initMountPoints', ['user' => $user->getUID()]);
+ $this->eventLogger->end('fs:setup:user:post-init-mountpoint');
+
+ $userDir = '/' . $user->getUID() . '/files';
+ $this->eventLogger->start('fs:setup:user:setup-hook', 'setup legacy hook');
+ OC_Hook::emit('OC_Filesystem', 'setup', ['user' => $user->getUID(), 'user_dir' => $userDir]);
+ $this->eventLogger->end('fs:setup:user:setup-hook');
+ }
+
+ /**
+ * Set up the root filesystem
+ */
+ public function setupRoot(): void {
+ //setting up the filesystem twice can only lead to trouble
+ if ($this->rootSetup) {
+ return;
+ }
+
+ $this->setupBuiltinWrappers();
+
+ $this->rootSetup = true;
+
+ $this->eventLogger->start('fs:setup:root', 'Setup root filesystem');
+
+ $rootMounts = $this->mountProviderCollection->getRootMounts();
+ foreach ($rootMounts as $rootMountProvider) {
+ $this->mountManager->addMount($rootMountProvider);
+ }
+
+ $this->eventLogger->end('fs:setup:root');
+ }
+
+ /**
+ * Get the user to setup for a path or `null` if the root needs to be setup
+ *
+ * @param string $path
+ * @return IUser|null
+ */
+ private function getUserForPath(string $path) {
+ if (str_starts_with($path, '/__groupfolders')) {
+ return null;
+ } elseif (substr_count($path, '/') < 2) {
+ if ($user = $this->userSession->getUser()) {
+ return $user;
+ } else {
+ return null;
+ }
+ } elseif (str_starts_with($path, '/appdata_' . \OC_Util::getInstanceId()) || str_starts_with($path, '/files_external/')) {
+ return null;
+ } else {
+ [, $userId] = explode('/', $path);
+ }
+
+ return $this->userManager->get($userId);
+ }
+
+ /**
+ * Set up the filesystem for the specified path
+ */
+ public function setupForPath(string $path, bool $includeChildren = false): void {
+ $user = $this->getUserForPath($path);
+ if (!$user) {
+ $this->setupRoot();
+ return;
+ }
+
+ if ($this->isSetupComplete($user)) {
+ return;
+ }
+
+ if ($this->fullSetupRequired($user)) {
+ $this->setupForUser($user);
+ return;
+ }
+
+ // for the user's home folder, and includes children we need everything always
+ if (rtrim($path) === '/' . $user->getUID() . '/files' && $includeChildren) {
+ $this->setupForUser($user);
+ return;
+ }
+
+ if (!isset($this->setupUserMountProviders[$user->getUID()])) {
+ $this->setupUserMountProviders[$user->getUID()] = [];
+ }
+ $setupProviders = &$this->setupUserMountProviders[$user->getUID()];
+ $currentProviders = [];
+
+ try {
+ $cachedMount = $this->userMountCache->getMountForPath($user, $path);
+ } catch (NotFoundException $e) {
+ $this->setupForUser($user);
+ return;
+ }
+
+ $this->oneTimeUserSetup($user);
+
+ $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 {
+ $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 ($includeChildren) {
+ $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);
+
+ 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()]));
+ }
+ }
+ }
+ } 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']);
+ });
+ } elseif (!$this->isSetupStarted($user)) {
+ $this->oneTimeUserSetup($user);
+ }
+ $this->eventLogger->end('fs:setup:user:path');
+ }
+
+ private function fullSetupRequired(IUser $user): bool {
+ if ($this->forceFullSetup) {
+ return true;
+ }
+
+ // we perform a "cached" setup only after having done the full setup recently
+ // this is also used to trigger a full setup after handling events that are likely
+ // to change the available mounts
+ if (!isset($this->fullSetupRequired[$user->getUID()])) {
+ $this->fullSetupRequired[$user->getUID()] = !$this->cache->get($user->getUID());
+ }
+ return $this->fullSetupRequired[$user->getUID()];
+ }
+
+ /**
+ * @param string $path
+ * @param string[] $providers
+ */
+ public function setupForProvider(string $path, array $providers): void {
+ $user = $this->getUserForPath($path);
+ if (!$user) {
+ $this->setupRoot();
+ return;
+ }
+
+ if ($this->isSetupComplete($user)) {
+ return;
+ }
+
+ if ($this->fullSetupRequired($user)) {
+ $this->setupForUser($user);
+ return;
+ }
+
+ $this->eventLogger->start('fs:setup:user:providers', 'Setup filesystem for ' . implode(', ', $providers));
+
+ $this->oneTimeUserSetup($user);
+
+ // home providers are always used
+ $providers = array_filter($providers, function (string $provider) {
+ return !is_subclass_of($provider, IHomeMountProvider::class);
+ });
+
+ if (in_array('', $providers)) {
+ $this->setupForUser($user);
+ return;
+ }
+ $setupProviders = $this->setupUserMountProviders[$user->getUID()] ?? [];
+
+ $providers = array_diff($providers, $setupProviders);
+ if (count($providers) === 0) {
+ if (!$this->isSetupStarted($user)) {
+ $this->oneTimeUserSetup($user);
+ }
+ $this->eventLogger->end('fs:setup:user:providers');
+ return;
+ } else {
+ $this->setupUserMountProviders[$user->getUID()] = array_merge($setupProviders, $providers);
+ $mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, $providers);
+ }
+
+ $this->registerMounts($user, $mounts, $providers);
+ $this->setupForUserWith($user, function () use ($mounts) {
+ array_walk($mounts, [$this->mountManager, 'addMount']);
+ });
+ $this->eventLogger->end('fs:setup:user:providers');
+ }
+
+ public function tearDown() {
+ $this->setupUsers = [];
+ $this->setupUsersComplete = [];
+ $this->setupUserMountProviders = [];
+ $this->fullSetupRequired = [];
+ $this->rootSetup = false;
+ $this->mountManager->clear();
+ $this->eventDispatcher->dispatchTyped(new FilesystemTornDownEvent());
+ }
+
+ /**
+ * Get mounts from mount providers that are registered after setup
+ */
+ private function listenForNewMountProviders() {
+ if (!$this->listeningForProviders) {
+ $this->listeningForProviders = true;
+ $this->mountProviderCollection->listen('\OC\Files\Config', 'registerMountProvider', function (
+ IMountProvider $provider,
+ ) {
+ foreach ($this->setupUsers as $userId) {
+ $user = $this->userManager->get($userId);
+ if ($user) {
+ $mounts = $provider->getMountsForUser($user, Filesystem::getLoader());
+ array_walk($mounts, [$this->mountManager, 'addMount']);
+ }
+ }
+ });
+ }
+ }
+
+ private function setupListeners() {
+ // note that this event handling is intentionally pessimistic
+ // clearing the cache to often is better than not enough
+
+ $this->eventDispatcher->addListener(UserAddedEvent::class, function (UserAddedEvent $event) {
+ $this->cache->remove($event->getUser()->getUID());
+ });
+ $this->eventDispatcher->addListener(UserRemovedEvent::class, function (UserRemovedEvent $event) {
+ $this->cache->remove($event->getUser()->getUID());
+ });
+ $this->eventDispatcher->addListener(ShareCreatedEvent::class, function (ShareCreatedEvent $event) {
+ $this->cache->remove($event->getShare()->getSharedWith());
+ });
+ $this->eventDispatcher->addListener(InvalidateMountCacheEvent::class, function (InvalidateMountCacheEvent $event,
+ ) {
+ if ($user = $event->getUser()) {
+ $this->cache->remove($user->getUID());
+ } else {
+ $this->cache->clear();
+ }
+ });
+
+ $genericEvents = [
+ 'OCA\Circles\Events\CreatingCircleEvent',
+ 'OCA\Circles\Events\DestroyingCircleEvent',
+ 'OCA\Circles\Events\AddingCircleMemberEvent',
+ 'OCA\Circles\Events\RemovingCircleMemberEvent',
+ ];
+
+ foreach ($genericEvents as $genericEvent) {
+ $this->eventDispatcher->addListener($genericEvent, function ($event) {
+ $this->cache->clear();
+ });
+ }
+ }
+
+ private function registerMounts(IUser $user, array $mounts, ?array $mountProviderClasses = null): void {
+ if ($this->lockdownManager->canAccessFilesystem()) {
+ $this->userMountCache->registerMounts($user, $mounts, $mountProviderClasses);
+ }
+ }
+}