123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598 |
- <?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_App;
- use OC_Hook;
- use OC_Util;
- use OCA\Files_External\Config\ConfigAdapter;
- use OCA\Files_Sharing\External\Mount;
- use OCA\Files_Sharing\ISharedMountPoint;
- use OCA\Files_Sharing\SharedMount;
- 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\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,
- ) {
- $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
- OC_App::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 ConfigAdapter || $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 OC_Util::getUserQuota($user);
- }, '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 $provider) {
- return get_class($provider);
- }, $this->mountProviderCollection->getProviders());
- $newProviders = array_diff($allProviders, $previouslySetupProviders);
- $mounts = array_filter($mounts, function (IMountPoint $mount) use ($previouslySetupProviders) {
- return !in_array($mount->getMountProvider(), $previouslySetupProviders);
- });
- $this->userMountCache->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->userMountCache->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->userMountCache->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();
- });
- }
- }
- }
|