nextcloud/lib/private/Files/SetupManager.php
Robin Appelman bd91c56539
get quota for user only when needed
always apply the wrapper but have the wrapper get the quota lazily only when an operation where quota is applied is performed

Signed-off-by: Robin Appelman <robin@icewind.nl>
2022-09-01 19:32:24 +02:00

578 lines
18 KiB
PHP

<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\Files;
use OC\Files\Config\MountProviderCollection;
use OC\Files\Mount\MountPoint;
use OC\Files\ObjectStore\HomeObjectStoreStorage;
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_App;
use OC_Hook;
use OC_Util;
use OCP\Constants;
use OCP\Diagnostics\IEventLogger;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Config\IHomeMountProvider;
use OCP\Files\Config\IMountProvider;
use OCP\Files\Config\IUserMountCache;
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;
private IEventLogger $eventLogger;
private MountProviderCollection $mountProviderCollection;
private IMountManager $mountManager;
private IUserManager $userManager;
// 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 IEventDispatcher $eventDispatcher;
private IUserMountCache $userMountCache;
private ILockdownManager $lockdownManager;
private IUserSession $userSession;
private ICache $cache;
private LoggerInterface $logger;
private IConfig $config;
private bool $listeningForProviders;
private array $fullSetupRequired = [];
private bool $setupBuiltinWrappersDone = false;
public function __construct(
IEventLogger $eventLogger,
MountProviderCollection $mountProviderCollection,
IMountManager $mountManager,
IUserManager $userManager,
IEventDispatcher $eventDispatcher,
IUserMountCache $userMountCache,
ILockdownManager $lockdownManager,
IUserSession $userSession,
ICacheFactory $cacheFactory,
LoggerInterface $logger,
IConfig $config
) {
$this->eventLogger = $eventLogger;
$this->mountProviderCollection = $mountProviderCollection;
$this->mountManager = $mountManager;
$this->userManager = $userManager;
$this->eventDispatcher = $eventDispatcher;
$this->userMountCache = $userMountCache;
$this->lockdownManager = $lockdownManager;
$this->logger = $logger;
$this->userSession = $userSession;
$this->cache = $cacheFactory->createDistributed('setupmanager::');
$this->listeningForProviders = false;
$this->config = $config;
$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)) {
$storage->setMountOptions($mount->getOptions());
}
return $storage;
});
Filesystem::addStorageWrapper('enable_sharing', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
if (!$mount->getOption('enable_sharing', true)) {
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) {
if (!$storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage') && !$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) && !$storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
return new Encoding(['storage' => $storage]);
}
return $storage;
});
Filesystem::addStorageWrapper('oc_quota', function ($mountPoint, $storage) {
// set up quota for home storages, even for other users
// which can happen when using sharing
/**
* @var Storage $storage
*/
if ($storage->instanceOfStorage(HomeObjectStoreStorage::class) || $storage->instanceOfStorage(Home::class)) {
if (is_object($storage->getUser())) {
$user = $storage->getUser();
return new Quota(['storage' => $storage, 'quotaCallback' => function () use ($user) {
return OC_Util::getUserQuota($user);
}, 'root' => 'files']);
}
}
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();
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);
}
/**
* part of the user setup that is run only once per user
*/
private function oneTimeUserSetup(IUser $user) {
if (in_array($user->getUID(), $this->setupUsers, true)) {
return;
}
$this->setupUsers[] = $user->getUID();
$this->setupBuiltinWrappers();
$prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
OC_Hook::emit('OC_Filesystem', 'preSetup', ['user' => $user->getUID()]);
Filesystem::logWarningWhenAddingStorageWrapper($prevLogging);
$userDir = '/' . $user->getUID() . '/files';
Filesystem::initInternal($userDir);
if ($this->lockdownManager->canAccessFilesystem()) {
// 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) {
$homeMount->getStorage()->mkdir('');
$homeMount->getStorage()->getScanner()->scan('');
}
} 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();
}
/**
* Final housekeeping after a user has been fully setup
*/
private function afterUserFullySetup(IUser $user, array $previouslySetupProviders): void {
$userRoot = '/' . $user->getUID() . '/';
$mounts = $this->mountManager->getAll();
$mounts = array_filter($mounts, function (IMountPoint $mount) use ($userRoot) {
return strpos($mount->getMountPoint(), $userRoot) === 0;
});
$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;
}
}
/**
* @param IUser $user
* @param IMountPoint $mounts
* @return void
* @throws \OCP\HintException
* @throws \OC\ServerNotAvailableException
*/
private function setupForUserWith(IUser $user, callable $mountCallback): void {
$this->setupRoot();
if (!$this->isSetupStarted($user)) {
$this->oneTimeUserSetup($user);
}
$this->eventLogger->start('setup_fs', 'Setup filesystem');
if ($this->lockdownManager->canAccessFilesystem()) {
$mountCallback();
}
\OC_Hook::emit('OC_Filesystem', 'post_initMountPoints', ['user' => $user->getUID()]);
$userDir = '/' . $user->getUID() . '/files';
OC_Hook::emit('OC_Filesystem', 'setup', ['user' => $user->getUID(), 'user_dir' => $userDir]);
$this->eventLogger->end('setup_fs');
}
/**
* Set up the root filesystem
*/
public function setupRoot(): void {
//setting up the filesystem twice can only lead to trouble
if ($this->rootSetup) {
return;
}
$this->rootSetup = true;
$this->eventLogger->start('setup_root_fs', 'Setup root filesystem');
$this->setupBuiltinWrappers();
$rootMounts = $this->mountProviderCollection->getRootMounts();
foreach ($rootMounts as $rootMountProvider) {
$this->mountManager->addMount($rootMountProvider);
}
$this->eventLogger->end('setup_root_fs');
}
/**
* 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 (strpos($path, '/__groupfolders') === 0) {
return null;
} elseif (substr_count($path, '/') < 2) {
if ($user = $this->userSession->getUser()) {
return $user;
} else {
return null;
}
} elseif (strpos($path, '/appdata_' . \OC_Util::getInstanceId()) === 0 || strpos($path, '/files_external/') === 0) {
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;
}
if (!$this->isSetupStarted($user)) {
$this->oneTimeUserSetup($user);
}
$mounts = [];
if (!in_array($cachedMount->getMountProvider(), $setupProviders)) {
$setupProviders[] = $cachedMount->getMountProvider();
$currentProviders[] = $cachedMount->getMountProvider();
if ($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->setupForUser($user);
return;
}
}
if ($includeChildren) {
$subCachedMounts = $this->userMountCache->getMountsInPath($user, $path);
foreach ($subCachedMounts as $cachedMount) {
if (!in_array($cachedMount->getMountProvider(), $setupProviders)) {
$setupProviders[] = $cachedMount->getMountProvider();
$currentProviders[] = $cachedMount->getMountProvider();
if ($cachedMount->getMountProvider()) {
$mounts = array_merge($mounts, $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]));
} else {
$this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup");
$this->setupForUser($user);
return;
}
}
}
}
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);
}
}
private function fullSetupRequired(IUser $user): bool {
// 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;
}
// 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);
}
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']);
});
}
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();
});
}
}
}