Root.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC\Files\Node;
  8. use OC\Files\FileInfo;
  9. use OC\Files\Mount\Manager;
  10. use OC\Files\Mount\MountPoint;
  11. use OC\Files\Utils\PathHelper;
  12. use OC\Files\View;
  13. use OC\Hooks\PublicEmitter;
  14. use OC\User\NoUserException;
  15. use OCP\Cache\CappedMemoryCache;
  16. use OCP\EventDispatcher\IEventDispatcher;
  17. use OCP\Files\Cache\ICacheEntry;
  18. use OCP\Files\Config\IUserMountCache;
  19. use OCP\Files\Events\Node\FilesystemTornDownEvent;
  20. use OCP\Files\IRootFolder;
  21. use OCP\Files\Mount\IMountPoint;
  22. use OCP\Files\Node as INode;
  23. use OCP\Files\NotFoundException;
  24. use OCP\Files\NotPermittedException;
  25. use OCP\ICache;
  26. use OCP\ICacheFactory;
  27. use OCP\IUser;
  28. use OCP\IUserManager;
  29. use Psr\Log\LoggerInterface;
  30. /**
  31. * Class Root
  32. *
  33. * Hooks available in scope \OC\Files
  34. * - preWrite(\OCP\Files\Node $node)
  35. * - postWrite(\OCP\Files\Node $node)
  36. * - preCreate(\OCP\Files\Node $node)
  37. * - postCreate(\OCP\Files\Node $node)
  38. * - preDelete(\OCP\Files\Node $node)
  39. * - postDelete(\OCP\Files\Node $node)
  40. * - preTouch(\OC\FilesP\Node $node, int $mtime)
  41. * - postTouch(\OCP\Files\Node $node)
  42. * - preCopy(\OCP\Files\Node $source, \OCP\Files\Node $target)
  43. * - postCopy(\OCP\Files\Node $source, \OCP\Files\Node $target)
  44. * - preRename(\OCP\Files\Node $source, \OCP\Files\Node $target)
  45. * - postRename(\OCP\Files\Node $source, \OCP\Files\Node $target)
  46. *
  47. * @package OC\Files\Node
  48. */
  49. class Root extends Folder implements IRootFolder {
  50. private Manager $mountManager;
  51. private PublicEmitter $emitter;
  52. private ?IUser $user;
  53. private CappedMemoryCache $userFolderCache;
  54. private IUserMountCache $userMountCache;
  55. private LoggerInterface $logger;
  56. private IUserManager $userManager;
  57. private IEventDispatcher $eventDispatcher;
  58. private ICache $pathByIdCache;
  59. /**
  60. * @param Manager $manager
  61. * @param View $view
  62. * @param IUser|null $user
  63. */
  64. public function __construct(
  65. $manager,
  66. $view,
  67. $user,
  68. IUserMountCache $userMountCache,
  69. LoggerInterface $logger,
  70. IUserManager $userManager,
  71. IEventDispatcher $eventDispatcher,
  72. ICacheFactory $cacheFactory,
  73. ) {
  74. parent::__construct($this, $view, '');
  75. $this->mountManager = $manager;
  76. $this->user = $user;
  77. $this->emitter = new PublicEmitter();
  78. $this->userFolderCache = new CappedMemoryCache();
  79. $this->userMountCache = $userMountCache;
  80. $this->logger = $logger;
  81. $this->userManager = $userManager;
  82. $eventDispatcher->addListener(FilesystemTornDownEvent::class, function () {
  83. $this->userFolderCache = new CappedMemoryCache();
  84. });
  85. $this->pathByIdCache = $cacheFactory->createLocal('path-by-id');
  86. }
  87. /**
  88. * Get the user for which the filesystem is setup
  89. *
  90. * @return \OC\User\User
  91. */
  92. public function getUser() {
  93. return $this->user;
  94. }
  95. /**
  96. * @param string $scope
  97. * @param string $method
  98. * @param callable $callback
  99. */
  100. public function listen($scope, $method, callable $callback) {
  101. $this->emitter->listen($scope, $method, $callback);
  102. }
  103. /**
  104. * @param string $scope optional
  105. * @param string $method optional
  106. * @param callable $callback optional
  107. */
  108. public function removeListener($scope = null, $method = null, ?callable $callback = null) {
  109. $this->emitter->removeListener($scope, $method, $callback);
  110. }
  111. /**
  112. * @param string $scope
  113. * @param string $method
  114. * @param Node[] $arguments
  115. */
  116. public function emit($scope, $method, $arguments = []) {
  117. $this->emitter->emit($scope, $method, $arguments);
  118. }
  119. /**
  120. * @param \OC\Files\Storage\Storage $storage
  121. * @param string $mountPoint
  122. * @param array $arguments
  123. */
  124. public function mount($storage, $mountPoint, $arguments = []) {
  125. $mount = new MountPoint($storage, $mountPoint, $arguments);
  126. $this->mountManager->addMount($mount);
  127. }
  128. public function getMount(string $mountPoint): IMountPoint {
  129. return $this->mountManager->find($mountPoint);
  130. }
  131. /**
  132. * @param string $mountPoint
  133. * @return \OC\Files\Mount\MountPoint[]
  134. */
  135. public function getMountsIn(string $mountPoint): array {
  136. return $this->mountManager->findIn($mountPoint);
  137. }
  138. /**
  139. * @param string $storageId
  140. * @return \OC\Files\Mount\MountPoint[]
  141. */
  142. public function getMountByStorageId($storageId) {
  143. return $this->mountManager->findByStorageId($storageId);
  144. }
  145. /**
  146. * @param int $numericId
  147. * @return MountPoint[]
  148. */
  149. public function getMountByNumericStorageId($numericId) {
  150. return $this->mountManager->findByNumericId($numericId);
  151. }
  152. /**
  153. * @param \OC\Files\Mount\MountPoint $mount
  154. */
  155. public function unMount($mount) {
  156. $this->mountManager->remove($mount);
  157. }
  158. /**
  159. * @param string $path
  160. * @return Node
  161. * @throws \OCP\Files\NotPermittedException
  162. * @throws \OCP\Files\NotFoundException
  163. */
  164. public function get($path) {
  165. $path = $this->normalizePath($path);
  166. if ($this->isValidPath($path)) {
  167. $fullPath = $this->getFullPath($path);
  168. $fileInfo = $this->view->getFileInfo($fullPath, false);
  169. if ($fileInfo) {
  170. return $this->createNode($fullPath, $fileInfo, false);
  171. } else {
  172. throw new NotFoundException($path);
  173. }
  174. } else {
  175. throw new NotPermittedException();
  176. }
  177. }
  178. //most operations can't be done on the root
  179. /**
  180. * @param string $targetPath
  181. * @return Node
  182. * @throws \OCP\Files\NotPermittedException
  183. */
  184. public function rename($targetPath) {
  185. throw new NotPermittedException();
  186. }
  187. public function delete() {
  188. throw new NotPermittedException();
  189. }
  190. /**
  191. * @param string $targetPath
  192. * @return Node
  193. * @throws \OCP\Files\NotPermittedException
  194. */
  195. public function copy($targetPath) {
  196. throw new NotPermittedException();
  197. }
  198. /**
  199. * @param int $mtime
  200. * @throws \OCP\Files\NotPermittedException
  201. */
  202. public function touch($mtime = null) {
  203. throw new NotPermittedException();
  204. }
  205. /**
  206. * @return \OC\Files\Storage\Storage
  207. * @throws \OCP\Files\NotFoundException
  208. */
  209. public function getStorage() {
  210. throw new NotFoundException();
  211. }
  212. /**
  213. * @return string
  214. */
  215. public function getPath() {
  216. return '/';
  217. }
  218. /**
  219. * @return string
  220. */
  221. public function getInternalPath() {
  222. return '';
  223. }
  224. /**
  225. * @return int
  226. */
  227. public function getId() {
  228. return 0;
  229. }
  230. /**
  231. * @return array
  232. */
  233. public function stat() {
  234. return [];
  235. }
  236. /**
  237. * @return int
  238. */
  239. public function getMTime() {
  240. return 0;
  241. }
  242. /**
  243. * @param bool $includeMounts
  244. * @return int|float
  245. */
  246. public function getSize($includeMounts = true): int|float {
  247. return 0;
  248. }
  249. /**
  250. * @return string
  251. */
  252. public function getEtag() {
  253. return '';
  254. }
  255. /**
  256. * @return int
  257. */
  258. public function getPermissions() {
  259. return \OCP\Constants::PERMISSION_CREATE;
  260. }
  261. /**
  262. * @return bool
  263. */
  264. public function isReadable() {
  265. return false;
  266. }
  267. /**
  268. * @return bool
  269. */
  270. public function isUpdateable() {
  271. return false;
  272. }
  273. /**
  274. * @return bool
  275. */
  276. public function isDeletable() {
  277. return false;
  278. }
  279. /**
  280. * @return bool
  281. */
  282. public function isShareable() {
  283. return false;
  284. }
  285. /**
  286. * @throws \OCP\Files\NotFoundException
  287. */
  288. public function getParent(): INode|IRootFolder {
  289. throw new NotFoundException();
  290. }
  291. /**
  292. * @return string
  293. */
  294. public function getName() {
  295. return '';
  296. }
  297. /**
  298. * Returns a view to user's files folder
  299. *
  300. * @param string $userId user ID
  301. * @return \OCP\Files\Folder
  302. * @throws NoUserException
  303. * @throws NotPermittedException
  304. */
  305. public function getUserFolder($userId) {
  306. $userObject = $this->userManager->get($userId);
  307. if (is_null($userObject)) {
  308. $e = new NoUserException('Backends provided no user object');
  309. $this->logger->error(
  310. sprintf(
  311. 'Backends provided no user object for %s',
  312. $userId
  313. ),
  314. [
  315. 'app' => 'files',
  316. 'exception' => $e,
  317. ]
  318. );
  319. throw $e;
  320. }
  321. $userId = $userObject->getUID();
  322. if (!$this->userFolderCache->hasKey($userId)) {
  323. if ($this->mountManager->getSetupManager()->isSetupComplete($userObject)) {
  324. try {
  325. $folder = $this->get('/' . $userId . '/files');
  326. if (!$folder instanceof \OCP\Files\Folder) {
  327. throw new \Exception("Account folder for \"$userId\" exists as a file");
  328. }
  329. } catch (NotFoundException $e) {
  330. if (!$this->nodeExists('/' . $userId)) {
  331. $this->newFolder('/' . $userId);
  332. }
  333. $folder = $this->newFolder('/' . $userId . '/files');
  334. }
  335. } else {
  336. $folder = new LazyUserFolder($this, $userObject, $this->mountManager);
  337. }
  338. $this->userFolderCache->set($userId, $folder);
  339. }
  340. return $this->userFolderCache->get($userId);
  341. }
  342. public function getUserMountCache() {
  343. return $this->userMountCache;
  344. }
  345. public function getFirstNodeByIdInPath(int $id, string $path): ?INode {
  346. // scope the cache by user, so we don't return nodes for different users
  347. if ($this->user) {
  348. $cachedPath = $this->pathByIdCache->get($this->user->getUID() . '::' . $id);
  349. if ($cachedPath && str_starts_with($path, $cachedPath)) {
  350. // getting the node by path is significantly cheaper than finding it by id
  351. $node = $this->get($cachedPath);
  352. // by validating that the cached path still has the requested fileid we can work around the need to invalidate the cached path
  353. // if the cached path is invalid or a different file now we fall back to the uncached logic
  354. if ($node && $node->getId() === $id) {
  355. return $node;
  356. }
  357. }
  358. }
  359. $node = current($this->getByIdInPath($id, $path));
  360. if (!$node) {
  361. return null;
  362. }
  363. if ($this->user) {
  364. $this->pathByIdCache->set($this->user->getUID() . '::' . $id, $node->getPath());
  365. }
  366. return $node;
  367. }
  368. /**
  369. * @param int $id
  370. * @return Node[]
  371. */
  372. public function getByIdInPath(int $id, string $path): array {
  373. $mountCache = $this->getUserMountCache();
  374. if (strpos($path, '/', 1) > 0) {
  375. [, $user] = explode('/', $path);
  376. } else {
  377. $user = null;
  378. }
  379. $mountsContainingFile = $mountCache->getMountsForFileId($id, $user);
  380. // if the mount isn't in the cache yet, perform a setup first, then try again
  381. if (count($mountsContainingFile) === 0) {
  382. $this->mountManager->getSetupManager()->setupForPath($path, true);
  383. $mountsContainingFile = $mountCache->getMountsForFileId($id, $user);
  384. }
  385. // when a user has access through the same storage through multiple paths
  386. // (such as an external storage that is both mounted for a user and shared to the user)
  387. // the mount cache will only hold a single entry for the storage
  388. // this can lead to issues as the different ways the user has access to a storage can have different permissions
  389. //
  390. // so instead of using the cached entries directly, we instead filter the current mounts by the rootid of the cache entry
  391. $mountRootIds = array_map(function ($mount) {
  392. return $mount->getRootId();
  393. }, $mountsContainingFile);
  394. $mountRootPaths = array_map(function ($mount) {
  395. return $mount->getRootInternalPath();
  396. }, $mountsContainingFile);
  397. $mountProviders = array_unique(array_map(function ($mount) {
  398. return $mount->getMountProvider();
  399. }, $mountsContainingFile));
  400. $mountRoots = array_combine($mountRootIds, $mountRootPaths);
  401. $mounts = $this->mountManager->getMountsByMountProvider($path, $mountProviders);
  402. $mountsContainingFile = array_filter($mounts, function ($mount) use ($mountRoots) {
  403. return isset($mountRoots[$mount->getStorageRootId()]);
  404. });
  405. if (count($mountsContainingFile) === 0) {
  406. if ($user === $this->getAppDataDirectoryName()) {
  407. $folder = $this->get($path);
  408. if ($folder instanceof Folder) {
  409. return $folder->getByIdInRootMount($id);
  410. } else {
  411. throw new \Exception("getByIdInPath with non folder");
  412. }
  413. }
  414. return [];
  415. }
  416. $nodes = array_map(function (IMountPoint $mount) use ($id, $mountRoots) {
  417. $rootInternalPath = $mountRoots[$mount->getStorageRootId()];
  418. $cacheEntry = $mount->getStorage()->getCache()->get($id);
  419. if (!$cacheEntry) {
  420. return null;
  421. }
  422. // cache jails will hide the "true" internal path
  423. $internalPath = ltrim($rootInternalPath . '/' . $cacheEntry->getPath(), '/');
  424. $pathRelativeToMount = substr($internalPath, strlen($rootInternalPath));
  425. $pathRelativeToMount = ltrim($pathRelativeToMount, '/');
  426. $absolutePath = rtrim($mount->getMountPoint() . $pathRelativeToMount, '/');
  427. return $this->createNode($absolutePath, new FileInfo(
  428. $absolutePath, $mount->getStorage(), $cacheEntry->getPath(), $cacheEntry, $mount,
  429. \OC::$server->getUserManager()->get($mount->getStorage()->getOwner($pathRelativeToMount))
  430. ));
  431. }, $mountsContainingFile);
  432. $nodes = array_filter($nodes);
  433. $folders = array_filter($nodes, function (Node $node) use ($path) {
  434. return PathHelper::getRelativePath($path, $node->getPath()) !== null;
  435. });
  436. usort($folders, function ($a, $b) {
  437. return $b->getPath() <=> $a->getPath();
  438. });
  439. return $folders;
  440. }
  441. public function getNodeFromCacheEntryAndMount(ICacheEntry $cacheEntry, IMountPoint $mountPoint): INode {
  442. $path = $cacheEntry->getPath();
  443. $fullPath = $mountPoint->getMountPoint() . $path;
  444. // todo: LazyNode?
  445. $info = new FileInfo($fullPath, $mountPoint->getStorage(), $path, $cacheEntry, $mountPoint);
  446. $parentPath = dirname($fullPath);
  447. $parent = new LazyFolder($this, function () use ($parentPath) {
  448. $parent = $this->get($parentPath);
  449. if ($parent instanceof \OCP\Files\Folder) {
  450. return $parent;
  451. } else {
  452. throw new \Exception("parent $parentPath is not a folder");
  453. }
  454. }, [
  455. 'path' => $parentPath,
  456. ]);
  457. $isDir = $info->getType() === FileInfo::TYPE_FOLDER;
  458. $view = new View('');
  459. if ($isDir) {
  460. return new Folder($this, $view, $path, $info, $parent);
  461. } else {
  462. return new File($this, $view, $path, $info, $parent);
  463. }
  464. }
  465. }