1
0

SetupManager.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl>
  5. *
  6. * @license GNU AGPL version 3 or any later version
  7. *
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU Affero General Public License as
  10. * published by the Free Software Foundation, either version 3 of the
  11. * License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. namespace OC\Files;
  23. use OC\Files\Config\MountProviderCollection;
  24. use OC\Files\Mount\MountPoint;
  25. use OC\Files\ObjectStore\HomeObjectStoreStorage;
  26. use OC\Files\Storage\Common;
  27. use OC\Files\Storage\Home;
  28. use OC\Files\Storage\Storage;
  29. use OC\Files\Storage\Wrapper\Availability;
  30. use OC\Files\Storage\Wrapper\Encoding;
  31. use OC\Files\Storage\Wrapper\PermissionsMask;
  32. use OC\Files\Storage\Wrapper\Quota;
  33. use OC\Lockdown\Filesystem\NullStorage;
  34. use OC_App;
  35. use OC_Hook;
  36. use OC_Util;
  37. use OCP\Constants;
  38. use OCP\Diagnostics\IEventLogger;
  39. use OCP\EventDispatcher\IEventDispatcher;
  40. use OCP\Files\Config\IHomeMountProvider;
  41. use OCP\Files\Config\IMountProvider;
  42. use OCP\Files\Config\IUserMountCache;
  43. use OCP\Files\Events\InvalidateMountCacheEvent;
  44. use OCP\Files\Events\Node\FilesystemTornDownEvent;
  45. use OCP\Files\Mount\IMountManager;
  46. use OCP\Files\Mount\IMountPoint;
  47. use OCP\Files\NotFoundException;
  48. use OCP\Files\Storage\IStorage;
  49. use OCP\Group\Events\UserAddedEvent;
  50. use OCP\Group\Events\UserRemovedEvent;
  51. use OCP\ICache;
  52. use OCP\ICacheFactory;
  53. use OCP\IConfig;
  54. use OCP\IUser;
  55. use OCP\IUserManager;
  56. use OCP\IUserSession;
  57. use OCP\Lockdown\ILockdownManager;
  58. use OCP\Share\Events\ShareCreatedEvent;
  59. use Psr\Log\LoggerInterface;
  60. class SetupManager {
  61. private bool $rootSetup = false;
  62. private IEventLogger $eventLogger;
  63. private MountProviderCollection $mountProviderCollection;
  64. private IMountManager $mountManager;
  65. private IUserManager $userManager;
  66. // List of users for which at least one mount is setup
  67. private array $setupUsers = [];
  68. // List of users for which all mounts are setup
  69. private array $setupUsersComplete = [];
  70. /** @var array<string, string[]> */
  71. private array $setupUserMountProviders = [];
  72. private IEventDispatcher $eventDispatcher;
  73. private IUserMountCache $userMountCache;
  74. private ILockdownManager $lockdownManager;
  75. private IUserSession $userSession;
  76. private ICache $cache;
  77. private LoggerInterface $logger;
  78. private IConfig $config;
  79. private bool $listeningForProviders;
  80. private array $fullSetupRequired = [];
  81. private bool $setupBuiltinWrappersDone = false;
  82. public function __construct(
  83. IEventLogger $eventLogger,
  84. MountProviderCollection $mountProviderCollection,
  85. IMountManager $mountManager,
  86. IUserManager $userManager,
  87. IEventDispatcher $eventDispatcher,
  88. IUserMountCache $userMountCache,
  89. ILockdownManager $lockdownManager,
  90. IUserSession $userSession,
  91. ICacheFactory $cacheFactory,
  92. LoggerInterface $logger,
  93. IConfig $config
  94. ) {
  95. $this->eventLogger = $eventLogger;
  96. $this->mountProviderCollection = $mountProviderCollection;
  97. $this->mountManager = $mountManager;
  98. $this->userManager = $userManager;
  99. $this->eventDispatcher = $eventDispatcher;
  100. $this->userMountCache = $userMountCache;
  101. $this->lockdownManager = $lockdownManager;
  102. $this->logger = $logger;
  103. $this->userSession = $userSession;
  104. $this->cache = $cacheFactory->createDistributed('setupmanager::');
  105. $this->listeningForProviders = false;
  106. $this->config = $config;
  107. $this->setupListeners();
  108. }
  109. private function isSetupStarted(IUser $user): bool {
  110. return in_array($user->getUID(), $this->setupUsers, true);
  111. }
  112. public function isSetupComplete(IUser $user): bool {
  113. return in_array($user->getUID(), $this->setupUsersComplete, true);
  114. }
  115. private function setupBuiltinWrappers() {
  116. if ($this->setupBuiltinWrappersDone) {
  117. return;
  118. }
  119. $this->setupBuiltinWrappersDone = true;
  120. // load all filesystem apps before, so no setup-hook gets lost
  121. OC_App::loadApps(['filesystem']);
  122. $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
  123. Filesystem::addStorageWrapper('mount_options', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
  124. if ($storage->instanceOfStorage(Common::class)) {
  125. $storage->setMountOptions($mount->getOptions());
  126. }
  127. return $storage;
  128. });
  129. Filesystem::addStorageWrapper('enable_sharing', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
  130. if (!$mount->getOption('enable_sharing', true)) {
  131. return new PermissionsMask([
  132. 'storage' => $storage,
  133. 'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE,
  134. ]);
  135. }
  136. return $storage;
  137. });
  138. // install storage availability wrapper, before most other wrappers
  139. Filesystem::addStorageWrapper('oc_availability', function ($mountPoint, IStorage $storage) {
  140. if (!$storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage') && !$storage->isLocal()) {
  141. return new Availability(['storage' => $storage]);
  142. }
  143. return $storage;
  144. });
  145. Filesystem::addStorageWrapper('oc_encoding', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
  146. if ($mount->getOption('encoding_compatibility', false) && !$storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
  147. return new Encoding(['storage' => $storage]);
  148. }
  149. return $storage;
  150. });
  151. Filesystem::addStorageWrapper('oc_quota', function ($mountPoint, $storage) {
  152. // set up quota for home storages, even for other users
  153. // which can happen when using sharing
  154. /**
  155. * @var Storage $storage
  156. */
  157. if ($storage->instanceOfStorage(HomeObjectStoreStorage::class) || $storage->instanceOfStorage(Home::class)) {
  158. if (is_object($storage->getUser())) {
  159. $user = $storage->getUser();
  160. return new Quota(['storage' => $storage, 'quotaCallback' => function () use ($user) {
  161. return OC_Util::getUserQuota($user);
  162. }, 'root' => 'files']);
  163. }
  164. }
  165. return $storage;
  166. });
  167. Filesystem::addStorageWrapper('readonly', function ($mountPoint, IStorage $storage, IMountPoint $mount) {
  168. /*
  169. * Do not allow any operations that modify the storage
  170. */
  171. if ($mount->getOption('readonly', false)) {
  172. return new PermissionsMask([
  173. 'storage' => $storage,
  174. 'mask' => Constants::PERMISSION_ALL & ~(
  175. Constants::PERMISSION_UPDATE |
  176. Constants::PERMISSION_CREATE |
  177. Constants::PERMISSION_DELETE
  178. ),
  179. ]);
  180. }
  181. return $storage;
  182. });
  183. Filesystem::logWarningWhenAddingStorageWrapper($prevLogging);
  184. }
  185. /**
  186. * Setup the full filesystem for the specified user
  187. */
  188. public function setupForUser(IUser $user): void {
  189. if ($this->isSetupComplete($user)) {
  190. return;
  191. }
  192. $this->setupUsersComplete[] = $user->getUID();
  193. if (!isset($this->setupUserMountProviders[$user->getUID()])) {
  194. $this->setupUserMountProviders[$user->getUID()] = [];
  195. }
  196. $previouslySetupProviders = $this->setupUserMountProviders[$user->getUID()];
  197. $this->setupForUserWith($user, function () use ($user) {
  198. $this->mountProviderCollection->addMountForUser($user, $this->mountManager, function (
  199. IMountProvider $provider
  200. ) use ($user) {
  201. return !in_array(get_class($provider), $this->setupUserMountProviders[$user->getUID()]);
  202. });
  203. });
  204. $this->afterUserFullySetup($user, $previouslySetupProviders);
  205. }
  206. /**
  207. * part of the user setup that is run only once per user
  208. */
  209. private function oneTimeUserSetup(IUser $user) {
  210. if (in_array($user->getUID(), $this->setupUsers, true)) {
  211. return;
  212. }
  213. $this->setupUsers[] = $user->getUID();
  214. $this->setupBuiltinWrappers();
  215. $prevLogging = Filesystem::logWarningWhenAddingStorageWrapper(false);
  216. OC_Hook::emit('OC_Filesystem', 'preSetup', ['user' => $user->getUID()]);
  217. Filesystem::logWarningWhenAddingStorageWrapper($prevLogging);
  218. $userDir = '/' . $user->getUID() . '/files';
  219. Filesystem::initInternal($userDir);
  220. if ($this->lockdownManager->canAccessFilesystem()) {
  221. // home mounts are handled separate since we need to ensure this is mounted before we call the other mount providers
  222. $homeMount = $this->mountProviderCollection->getHomeMountForUser($user);
  223. $this->mountManager->addMount($homeMount);
  224. if ($homeMount->getStorageRootId() === -1) {
  225. $homeMount->getStorage()->mkdir('');
  226. $homeMount->getStorage()->getScanner()->scan('');
  227. }
  228. } else {
  229. $this->mountManager->addMount(new MountPoint(
  230. new NullStorage([]),
  231. '/' . $user->getUID()
  232. ));
  233. $this->mountManager->addMount(new MountPoint(
  234. new NullStorage([]),
  235. '/' . $user->getUID() . '/files'
  236. ));
  237. $this->setupUsersComplete[] = $user->getUID();
  238. }
  239. $this->listenForNewMountProviders();
  240. }
  241. /**
  242. * Final housekeeping after a user has been fully setup
  243. */
  244. private function afterUserFullySetup(IUser $user, array $previouslySetupProviders): void {
  245. $userRoot = '/' . $user->getUID() . '/';
  246. $mounts = $this->mountManager->getAll();
  247. $mounts = array_filter($mounts, function (IMountPoint $mount) use ($userRoot) {
  248. return strpos($mount->getMountPoint(), $userRoot) === 0;
  249. });
  250. $allProviders = array_map(function (IMountProvider $provider) {
  251. return get_class($provider);
  252. }, $this->mountProviderCollection->getProviders());
  253. $newProviders = array_diff($allProviders, $previouslySetupProviders);
  254. $mounts = array_filter($mounts, function (IMountPoint $mount) use ($previouslySetupProviders) {
  255. return !in_array($mount->getMountProvider(), $previouslySetupProviders);
  256. });
  257. $this->userMountCache->registerMounts($user, $mounts, $newProviders);
  258. $cacheDuration = $this->config->getSystemValueInt('fs_mount_cache_duration', 5 * 60);
  259. if ($cacheDuration > 0) {
  260. $this->cache->set($user->getUID(), true, $cacheDuration);
  261. $this->fullSetupRequired[$user->getUID()] = false;
  262. }
  263. }
  264. /**
  265. * @param IUser $user
  266. * @param IMountPoint $mounts
  267. * @return void
  268. * @throws \OCP\HintException
  269. * @throws \OC\ServerNotAvailableException
  270. */
  271. private function setupForUserWith(IUser $user, callable $mountCallback): void {
  272. $this->setupRoot();
  273. if (!$this->isSetupStarted($user)) {
  274. $this->oneTimeUserSetup($user);
  275. }
  276. $this->eventLogger->start('setup_fs', 'Setup filesystem');
  277. if ($this->lockdownManager->canAccessFilesystem()) {
  278. $mountCallback();
  279. }
  280. \OC_Hook::emit('OC_Filesystem', 'post_initMountPoints', ['user' => $user->getUID()]);
  281. $userDir = '/' . $user->getUID() . '/files';
  282. OC_Hook::emit('OC_Filesystem', 'setup', ['user' => $user->getUID(), 'user_dir' => $userDir]);
  283. $this->eventLogger->end('setup_fs');
  284. }
  285. /**
  286. * Set up the root filesystem
  287. */
  288. public function setupRoot(): void {
  289. //setting up the filesystem twice can only lead to trouble
  290. if ($this->rootSetup) {
  291. return;
  292. }
  293. $this->rootSetup = true;
  294. $this->eventLogger->start('setup_root_fs', 'Setup root filesystem');
  295. $this->setupBuiltinWrappers();
  296. $rootMounts = $this->mountProviderCollection->getRootMounts();
  297. foreach ($rootMounts as $rootMountProvider) {
  298. $this->mountManager->addMount($rootMountProvider);
  299. }
  300. $this->eventLogger->end('setup_root_fs');
  301. }
  302. /**
  303. * Get the user to setup for a path or `null` if the root needs to be setup
  304. *
  305. * @param string $path
  306. * @return IUser|null
  307. */
  308. private function getUserForPath(string $path) {
  309. if (strpos($path, '/__groupfolders') === 0) {
  310. return null;
  311. } elseif (substr_count($path, '/') < 2) {
  312. if ($user = $this->userSession->getUser()) {
  313. return $user;
  314. } else {
  315. return null;
  316. }
  317. } elseif (strpos($path, '/appdata_' . \OC_Util::getInstanceId()) === 0 || strpos($path, '/files_external/') === 0) {
  318. return null;
  319. } else {
  320. [, $userId] = explode('/', $path);
  321. }
  322. return $this->userManager->get($userId);
  323. }
  324. /**
  325. * Set up the filesystem for the specified path
  326. */
  327. public function setupForPath(string $path, bool $includeChildren = false): void {
  328. $user = $this->getUserForPath($path);
  329. if (!$user) {
  330. $this->setupRoot();
  331. return;
  332. }
  333. if ($this->isSetupComplete($user)) {
  334. return;
  335. }
  336. if ($this->fullSetupRequired($user)) {
  337. $this->setupForUser($user);
  338. return;
  339. }
  340. // for the user's home folder, and includes children we need everything always
  341. if (rtrim($path) === "/" . $user->getUID() . "/files" && $includeChildren) {
  342. $this->setupForUser($user);
  343. return;
  344. }
  345. if (!isset($this->setupUserMountProviders[$user->getUID()])) {
  346. $this->setupUserMountProviders[$user->getUID()] = [];
  347. }
  348. $setupProviders = &$this->setupUserMountProviders[$user->getUID()];
  349. $currentProviders = [];
  350. try {
  351. $cachedMount = $this->userMountCache->getMountForPath($user, $path);
  352. } catch (NotFoundException $e) {
  353. $this->setupForUser($user);
  354. return;
  355. }
  356. if (!$this->isSetupStarted($user)) {
  357. $this->oneTimeUserSetup($user);
  358. }
  359. $mounts = [];
  360. if (!in_array($cachedMount->getMountProvider(), $setupProviders)) {
  361. $setupProviders[] = $cachedMount->getMountProvider();
  362. $currentProviders[] = $cachedMount->getMountProvider();
  363. if ($cachedMount->getMountProvider()) {
  364. $mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]);
  365. } else {
  366. $this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup");
  367. $this->setupForUser($user);
  368. return;
  369. }
  370. }
  371. if ($includeChildren) {
  372. $subCachedMounts = $this->userMountCache->getMountsInPath($user, $path);
  373. foreach ($subCachedMounts as $cachedMount) {
  374. if (!in_array($cachedMount->getMountProvider(), $setupProviders)) {
  375. $setupProviders[] = $cachedMount->getMountProvider();
  376. $currentProviders[] = $cachedMount->getMountProvider();
  377. if ($cachedMount->getMountProvider()) {
  378. $mounts = array_merge($mounts, $this->mountProviderCollection->getUserMountsForProviderClasses($user, [$cachedMount->getMountProvider()]));
  379. } else {
  380. $this->logger->debug("mount at " . $cachedMount->getMountPoint() . " has no provider set, performing full setup");
  381. $this->setupForUser($user);
  382. return;
  383. }
  384. }
  385. }
  386. }
  387. if (count($mounts)) {
  388. $this->userMountCache->registerMounts($user, $mounts, $currentProviders);
  389. $this->setupForUserWith($user, function () use ($mounts) {
  390. array_walk($mounts, [$this->mountManager, 'addMount']);
  391. });
  392. } elseif (!$this->isSetupStarted($user)) {
  393. $this->oneTimeUserSetup($user);
  394. }
  395. }
  396. private function fullSetupRequired(IUser $user): bool {
  397. // we perform a "cached" setup only after having done the full setup recently
  398. // this is also used to trigger a full setup after handling events that are likely
  399. // to change the available mounts
  400. if (!isset($this->fullSetupRequired[$user->getUID()])) {
  401. $this->fullSetupRequired[$user->getUID()] = !$this->cache->get($user->getUID());
  402. }
  403. return $this->fullSetupRequired[$user->getUID()];
  404. }
  405. /**
  406. * @param string $path
  407. * @param string[] $providers
  408. */
  409. public function setupForProvider(string $path, array $providers): void {
  410. $user = $this->getUserForPath($path);
  411. if (!$user) {
  412. $this->setupRoot();
  413. return;
  414. }
  415. if ($this->isSetupComplete($user)) {
  416. return;
  417. }
  418. if ($this->fullSetupRequired($user)) {
  419. $this->setupForUser($user);
  420. return;
  421. }
  422. // home providers are always used
  423. $providers = array_filter($providers, function (string $provider) {
  424. return !is_subclass_of($provider, IHomeMountProvider::class);
  425. });
  426. if (in_array('', $providers)) {
  427. $this->setupForUser($user);
  428. return;
  429. }
  430. $setupProviders = $this->setupUserMountProviders[$user->getUID()] ?? [];
  431. $providers = array_diff($providers, $setupProviders);
  432. if (count($providers) === 0) {
  433. if (!$this->isSetupStarted($user)) {
  434. $this->oneTimeUserSetup($user);
  435. }
  436. return;
  437. } else {
  438. $this->setupUserMountProviders[$user->getUID()] = array_merge($setupProviders, $providers);
  439. $mounts = $this->mountProviderCollection->getUserMountsForProviderClasses($user, $providers);
  440. }
  441. $this->userMountCache->registerMounts($user, $mounts, $providers);
  442. $this->setupForUserWith($user, function () use ($mounts) {
  443. array_walk($mounts, [$this->mountManager, 'addMount']);
  444. });
  445. }
  446. public function tearDown() {
  447. $this->setupUsers = [];
  448. $this->setupUsersComplete = [];
  449. $this->setupUserMountProviders = [];
  450. $this->fullSetupRequired = [];
  451. $this->rootSetup = false;
  452. $this->mountManager->clear();
  453. $this->eventDispatcher->dispatchTyped(new FilesystemTornDownEvent());
  454. }
  455. /**
  456. * Get mounts from mount providers that are registered after setup
  457. */
  458. private function listenForNewMountProviders() {
  459. if (!$this->listeningForProviders) {
  460. $this->listeningForProviders = true;
  461. $this->mountProviderCollection->listen('\OC\Files\Config', 'registerMountProvider', function (
  462. IMountProvider $provider
  463. ) {
  464. foreach ($this->setupUsers as $userId) {
  465. $user = $this->userManager->get($userId);
  466. if ($user) {
  467. $mounts = $provider->getMountsForUser($user, Filesystem::getLoader());
  468. array_walk($mounts, [$this->mountManager, 'addMount']);
  469. }
  470. }
  471. });
  472. }
  473. }
  474. private function setupListeners() {
  475. // note that this event handling is intentionally pessimistic
  476. // clearing the cache to often is better than not enough
  477. $this->eventDispatcher->addListener(UserAddedEvent::class, function (UserAddedEvent $event) {
  478. $this->cache->remove($event->getUser()->getUID());
  479. });
  480. $this->eventDispatcher->addListener(UserRemovedEvent::class, function (UserRemovedEvent $event) {
  481. $this->cache->remove($event->getUser()->getUID());
  482. });
  483. $this->eventDispatcher->addListener(ShareCreatedEvent::class, function (ShareCreatedEvent $event) {
  484. $this->cache->remove($event->getShare()->getSharedWith());
  485. });
  486. $this->eventDispatcher->addListener(InvalidateMountCacheEvent::class, function (InvalidateMountCacheEvent $event
  487. ) {
  488. if ($user = $event->getUser()) {
  489. $this->cache->remove($user->getUID());
  490. } else {
  491. $this->cache->clear();
  492. }
  493. });
  494. $genericEvents = [
  495. 'OCA\Circles\Events\CreatingCircleEvent',
  496. 'OCA\Circles\Events\DestroyingCircleEvent',
  497. 'OCA\Circles\Events\AddingCircleMemberEvent',
  498. 'OCA\Circles\Events\RemovingCircleMemberEvent',
  499. ];
  500. foreach ($genericEvents as $genericEvent) {
  501. $this->eventDispatcher->addListener($genericEvent, function ($event) {
  502. $this->cache->clear();
  503. });
  504. }
  505. }
  506. }