diff options
Diffstat (limited to 'lib/private/Files/Node')
-rw-r--r-- | lib/private/Files/Node/File.php | 140 | ||||
-rw-r--r-- | lib/private/Files/Node/Folder.php | 472 | ||||
-rw-r--r-- | lib/private/Files/Node/HookConnector.php | 227 | ||||
-rw-r--r-- | lib/private/Files/Node/LazyFolder.php | 568 | ||||
-rw-r--r-- | lib/private/Files/Node/LazyRoot.php | 56 | ||||
-rw-r--r-- | lib/private/Files/Node/LazyUserFolder.php | 66 | ||||
-rw-r--r-- | lib/private/Files/Node/Node.php | 489 | ||||
-rw-r--r-- | lib/private/Files/Node/NonExistingFile.php | 136 | ||||
-rw-r--r-- | lib/private/Files/Node/NonExistingFolder.php | 172 | ||||
-rw-r--r-- | lib/private/Files/Node/Root.php | 534 |
10 files changed, 2860 insertions, 0 deletions
diff --git a/lib/private/Files/Node/File.php b/lib/private/Files/Node/File.php new file mode 100644 index 00000000000..eb6411d7d13 --- /dev/null +++ b/lib/private/Files/Node/File.php @@ -0,0 +1,140 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Files\Node; + +use OCP\Files\GenericFileException; +use OCP\Files\NotPermittedException; +use OCP\Lock\LockedException; + +class File extends Node implements \OCP\Files\File { + /** + * Creates a Folder that represents a non-existing path + * + * @param string $path path + * @return NonExistingFile non-existing node + */ + protected function createNonExistingNode($path) { + return new NonExistingFile($this->root, $this->view, $path); + } + + /** + * @return string + * @throws NotPermittedException + * @throws GenericFileException + * @throws LockedException + */ + public function getContent() { + if ($this->checkPermissions(\OCP\Constants::PERMISSION_READ)) { + $content = $this->view->file_get_contents($this->path); + if ($content === false) { + throw new GenericFileException(); + } + return $content; + } else { + throw new NotPermittedException(); + } + } + + /** + * @param string|resource $data + * @throws NotPermittedException + * @throws GenericFileException + * @throws LockedException + */ + public function putContent($data) { + if ($this->checkPermissions(\OCP\Constants::PERMISSION_UPDATE)) { + $this->sendHooks(['preWrite']); + if ($this->view->file_put_contents($this->path, $data) === false) { + throw new GenericFileException('file_put_contents failed'); + } + $this->fileInfo = null; + $this->sendHooks(['postWrite']); + } else { + throw new NotPermittedException(); + } + } + + /** + * @param string $mode + * @return resource|false + * @throws NotPermittedException + * @throws LockedException + */ + public function fopen($mode) { + $preHooks = []; + $postHooks = []; + $requiredPermissions = \OCP\Constants::PERMISSION_READ; + switch ($mode) { + case 'r+': + case 'rb+': + case 'w+': + case 'wb+': + case 'x+': + case 'xb+': + case 'a+': + case 'ab+': + case 'w': + case 'wb': + case 'x': + case 'xb': + case 'a': + case 'ab': + $preHooks[] = 'preWrite'; + $postHooks[] = 'postWrite'; + $requiredPermissions |= \OCP\Constants::PERMISSION_UPDATE; + break; + } + + if ($this->checkPermissions($requiredPermissions)) { + $this->sendHooks($preHooks); + $result = $this->view->fopen($this->path, $mode); + $this->sendHooks($postHooks); + return $result; + } else { + throw new NotPermittedException(); + } + } + + /** + * @throws NotPermittedException + * @throws \OCP\Files\InvalidPathException + * @throws \OCP\Files\NotFoundException + */ + public function delete() { + if ($this->checkPermissions(\OCP\Constants::PERMISSION_DELETE)) { + $this->sendHooks(['preDelete']); + $fileInfo = $this->getFileInfo(); + $this->view->unlink($this->path); + $nonExisting = new NonExistingFile($this->root, $this->view, $this->path, $fileInfo); + $this->sendHooks(['postDelete'], [$nonExisting]); + $this->fileInfo = null; + } else { + throw new NotPermittedException(); + } + } + + /** + * @param string $type + * @param bool $raw + * @return string + */ + public function hash($type, $raw = false) { + return $this->view->hash($type, $this->path, $raw); + } + + /** + * @inheritdoc + */ + public function getChecksum() { + return $this->getFileInfo()->getChecksum(); + } + + public function getExtension(): string { + return $this->getFileInfo()->getExtension(); + } +} diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php new file mode 100644 index 00000000000..7453b553119 --- /dev/null +++ b/lib/private/Files/Node/Folder.php @@ -0,0 +1,472 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Files\Node; + +use OC\Files\Cache\QuerySearchHelper; +use OC\Files\Search\SearchBinaryOperator; +use OC\Files\Search\SearchComparison; +use OC\Files\Search\SearchOrder; +use OC\Files\Search\SearchQuery; +use OC\Files\Utils\PathHelper; +use OC\User\LazyUser; +use OCP\Files\Cache\ICacheEntry; +use OCP\Files\FileInfo; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\Node as INode; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; +use OCP\Files\Search\ISearchOperator; +use OCP\Files\Search\ISearchOrder; +use OCP\Files\Search\ISearchQuery; +use OCP\IUserManager; + +class Folder extends Node implements \OCP\Files\Folder { + + private ?IUserManager $userManager = null; + + /** + * Creates a Folder that represents a non-existing path + * + * @param string $path path + * @return NonExistingFolder non-existing node + */ + protected function createNonExistingNode($path) { + return new NonExistingFolder($this->root, $this->view, $path); + } + + /** + * @param string $path path relative to the folder + * @return string + * @throws \OCP\Files\NotPermittedException + */ + public function getFullPath($path) { + $path = $this->normalizePath($path); + if (!$this->isValidPath($path)) { + throw new NotPermittedException('Invalid path "' . $path . '"'); + } + return $this->path . $path; + } + + /** + * @param string $path + * @return string|null + */ + public function getRelativePath($path) { + return PathHelper::getRelativePath($this->getPath(), $path); + } + + /** + * check if a node is a (grand-)child of the folder + * + * @param \OC\Files\Node\Node $node + * @return bool + */ + public function isSubNode($node) { + return str_starts_with($node->getPath(), $this->path . '/'); + } + + /** + * get the content of this directory + * + * @return Node[] + * @throws \OCP\Files\NotFoundException + */ + public function getDirectoryListing() { + $folderContent = $this->view->getDirectoryContent($this->path, '', $this->getFileInfo(false)); + + return array_map(function (FileInfo $info) { + if ($info->getMimetype() === FileInfo::MIMETYPE_FOLDER) { + return new Folder($this->root, $this->view, $info->getPath(), $info, $this); + } else { + return new File($this->root, $this->view, $info->getPath(), $info, $this); + } + }, $folderContent); + } + + protected function createNode(string $path, ?FileInfo $info = null, bool $infoHasSubMountsIncluded = true): INode { + if (is_null($info)) { + $isDir = $this->view->is_dir($path); + } else { + $isDir = $info->getType() === FileInfo::TYPE_FOLDER; + } + $parent = dirname($path) === $this->getPath() ? $this : null; + if ($isDir) { + return new Folder($this->root, $this->view, $path, $info, $parent, $infoHasSubMountsIncluded); + } else { + return new File($this->root, $this->view, $path, $info, $parent); + } + } + + public function get($path) { + return $this->root->get($this->getFullPath($path)); + } + + public function nodeExists($path) { + try { + $this->get($path); + return true; + } catch (NotFoundException|NotPermittedException) { + return false; + } + } + + /** + * @param string $path + * @return \OC\Files\Node\Folder + * @throws \OCP\Files\NotPermittedException + */ + public function newFolder($path) { + if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) { + $fullPath = $this->getFullPath($path); + $nonExisting = new NonExistingFolder($this->root, $this->view, $fullPath); + $this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]); + if (!$this->view->mkdir($fullPath)) { + // maybe another concurrent process created the folder already + if (!$this->view->is_dir($fullPath)) { + throw new NotPermittedException('Could not create folder "' . $fullPath . '"'); + } else { + // we need to ensure we don't return before the concurrent request has finished updating the cache + $tries = 5; + while (!$this->view->getFileInfo($fullPath)) { + if ($tries < 1) { + throw new NotPermittedException('Could not create folder "' . $fullPath . '", folder exists but unable to get cache entry'); + } + usleep(5 * 1000); + $tries--; + } + } + } + $parent = dirname($fullPath) === $this->getPath() ? $this : null; + $node = new Folder($this->root, $this->view, $fullPath, null, $parent); + $this->sendHooks(['postWrite', 'postCreate'], [$node]); + return $node; + } else { + throw new NotPermittedException('No create permission for folder "' . $path . '"'); + } + } + + /** + * @param string $path + * @param string | resource | null $content + * @return \OC\Files\Node\File + * @throws \OCP\Files\NotPermittedException + */ + public function newFile($path, $content = null) { + if ($path === '') { + throw new NotPermittedException('Could not create as provided path is empty'); + } + if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) { + $fullPath = $this->getFullPath($path); + $nonExisting = new NonExistingFile($this->root, $this->view, $fullPath); + $this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]); + if ($content !== null) { + $result = $this->view->file_put_contents($fullPath, $content); + } else { + $result = $this->view->touch($fullPath); + } + if ($result === false) { + throw new NotPermittedException('Could not create path "' . $fullPath . '"'); + } + $node = new File($this->root, $this->view, $fullPath, null, $this); + $this->sendHooks(['postWrite', 'postCreate'], [$node]); + return $node; + } + throw new NotPermittedException('No create permission for path "' . $path . '"'); + } + + private function queryFromOperator(ISearchOperator $operator, ?string $uid = null, int $limit = 0, int $offset = 0): ISearchQuery { + if ($uid === null) { + $user = null; + } else { + /** @var IUserManager $userManager */ + $userManager = \OCP\Server::get(IUserManager::class); + $user = $userManager->get($uid); + } + return new SearchQuery($operator, $limit, $offset, [], $user); + } + + /** + * search for files with the name matching $query + * + * @param string|ISearchQuery $query + * @return \OC\Files\Node\Node[] + */ + public function search($query) { + if (is_string($query)) { + $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query . '%')); + } + + // search is handled by a single query covering all caches that this folder contains + // this is done by collect + + $limitToHome = $query->limitToHome(); + if ($limitToHome && count(explode('/', $this->path)) !== 3) { + throw new \InvalidArgumentException('searching by owner is only allowed in the users home folder'); + } + + /** @var QuerySearchHelper $searchHelper */ + $searchHelper = \OC::$server->get(QuerySearchHelper::class); + [$caches, $mountByMountPoint] = $searchHelper->getCachesAndMountPointsForSearch($this->root, $this->path, $limitToHome); + $resultsPerCache = $searchHelper->searchInCaches($query, $caches); + + // loop through all results per-cache, constructing the FileInfo object from the CacheEntry and merge them all + $files = array_merge(...array_map(function (array $results, string $relativeMountPoint) use ($mountByMountPoint) { + $mount = $mountByMountPoint[$relativeMountPoint]; + return array_map(function (ICacheEntry $result) use ($relativeMountPoint, $mount) { + return $this->cacheEntryToFileInfo($mount, $relativeMountPoint, $result); + }, $results); + }, array_values($resultsPerCache), array_keys($resultsPerCache))); + + // don't include this folder in the results + $files = array_values(array_filter($files, function (FileInfo $file) { + return $file->getPath() !== $this->getPath(); + })); + + // since results were returned per-cache, they are no longer fully sorted + $order = $query->getOrder(); + if ($order) { + usort($files, function (FileInfo $a, FileInfo $b) use ($order) { + foreach ($order as $orderField) { + $cmp = $orderField->sortFileInfo($a, $b); + if ($cmp !== 0) { + return $cmp; + } + } + return 0; + }); + } + + return array_map(function (FileInfo $file) { + return $this->createNode($file->getPath(), $file); + }, $files); + } + + private function cacheEntryToFileInfo(IMountPoint $mount, string $appendRoot, ICacheEntry $cacheEntry): FileInfo { + $cacheEntry['internalPath'] = $cacheEntry['path']; + $cacheEntry['path'] = rtrim($appendRoot . $cacheEntry->getPath(), '/'); + $subPath = $cacheEntry['path'] !== '' ? '/' . $cacheEntry['path'] : ''; + $storage = $mount->getStorage(); + + $owner = null; + $ownerId = $storage->getOwner($cacheEntry['internalPath']); + if ($ownerId !== false) { + // Cache the user manager (for performance) + if ($this->userManager === null) { + $this->userManager = \OCP\Server::get(IUserManager::class); + } + $owner = new LazyUser($ownerId, $this->userManager); + } + + return new \OC\Files\FileInfo( + $this->path . $subPath, + $storage, + $cacheEntry['internalPath'], + $cacheEntry, + $mount, + $owner, + ); + } + + /** + * search for files by mimetype + * + * @param string $mimetype + * @return Node[] + */ + public function searchByMime($mimetype) { + if (!str_contains($mimetype, '/')) { + $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%')); + } else { + $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype)); + } + return $this->search($query); + } + + /** + * search for files by tag + * + * @param string|int $tag name or tag id + * @param string $userId owner of the tags + * @return Node[] + */ + public function searchByTag($tag, $userId) { + $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'tagname', $tag), $userId); + return $this->search($query); + } + + public function searchBySystemTag(string $tagName, string $userId, int $limit = 0, int $offset = 0): array { + $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'systemtag', $tagName), $userId, $limit, $offset); + return $this->search($query); + } + + /** + * @param int $id + * @return \OCP\Files\Node[] + */ + public function getById($id) { + return $this->root->getByIdInPath((int)$id, $this->getPath()); + } + + public function getFirstNodeById(int $id): ?\OCP\Files\Node { + return $this->root->getFirstNodeByIdInPath($id, $this->getPath()); + } + + public function getAppDataDirectoryName(): string { + $instanceId = \OC::$server->getConfig()->getSystemValueString('instanceid'); + return 'appdata_' . $instanceId; + } + + /** + * In case the path we are currently in is inside the appdata_* folder, + * the original getById method does not work, because it can only look inside + * the user's mount points. But the user has no mount point for the root storage. + * + * So in that case we directly check the mount of the root if it contains + * the id. If it does we check if the path is inside the path we are working + * in. + * + * @param int $id + * @return array + */ + protected function getByIdInRootMount(int $id): array { + if (!method_exists($this->root, 'createNode')) { + // Always expected to be false. Being a method of Folder, this is + // always implemented. For it is an internal method and should not + // be exposed and made public, it is not part of an interface. + return []; + } + $mount = $this->root->getMount(''); + $storage = $mount->getStorage(); + $cacheEntry = $storage?->getCache($this->path)->get($id); + if (!$cacheEntry) { + return []; + } + + $absolutePath = '/' . ltrim($cacheEntry->getPath(), '/'); + $currentPath = rtrim($this->path, '/') . '/'; + + if (!str_starts_with($absolutePath, $currentPath)) { + return []; + } + + return [$this->root->createNode( + $absolutePath, new \OC\Files\FileInfo( + $absolutePath, + $storage, + $cacheEntry->getPath(), + $cacheEntry, + $mount + ))]; + } + + public function getFreeSpace() { + return $this->view->free_space($this->path); + } + + public function delete() { + if ($this->checkPermissions(\OCP\Constants::PERMISSION_DELETE)) { + $this->sendHooks(['preDelete']); + $fileInfo = $this->getFileInfo(); + $this->view->rmdir($this->path); + $nonExisting = new NonExistingFolder($this->root, $this->view, $this->path, $fileInfo); + $this->sendHooks(['postDelete'], [$nonExisting]); + } else { + throw new NotPermittedException('No delete permission for path "' . $this->path . '"'); + } + } + + /** + * Add a suffix to the name in case the file exists + * + * @param string $name + * @return string + * @throws NotPermittedException + */ + public function getNonExistingName($name) { + $uniqueName = \OC_Helper::buildNotExistingFileNameForView($this->getPath(), $name, $this->view); + return trim($this->getRelativePath($uniqueName), '/'); + } + + /** + * @param int $limit + * @param int $offset + * @return INode[] + */ + public function getRecent($limit, $offset = 0) { + $filterOutNonEmptyFolder = new SearchBinaryOperator( + // filter out non empty folders + ISearchBinaryOperator::OPERATOR_OR, + [ + new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_NOT, + [ + new SearchComparison( + ISearchComparison::COMPARE_EQUAL, + 'mimetype', + FileInfo::MIMETYPE_FOLDER + ), + ] + ), + new SearchComparison( + ISearchComparison::COMPARE_EQUAL, + 'size', + 0 + ), + ] + ); + + $filterNonRecentFiles = new SearchComparison( + ISearchComparison::COMPARE_GREATER_THAN, + 'mtime', + strtotime('-2 week') + ); + if ($offset === 0 && $limit <= 100) { + $query = new SearchQuery( + new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_AND, + [ + $filterOutNonEmptyFolder, + $filterNonRecentFiles, + ], + ), + $limit, + $offset, + [ + new SearchOrder( + ISearchOrder::DIRECTION_DESCENDING, + 'mtime' + ), + ] + ); + } else { + $query = new SearchQuery( + $filterOutNonEmptyFolder, + $limit, + $offset, + [ + new SearchOrder( + ISearchOrder::DIRECTION_DESCENDING, + 'mtime' + ), + ] + ); + } + + return $this->search($query); + } + + public function verifyPath($fileName, $readonly = false): void { + $this->view->verifyPath( + $this->getPath(), + $fileName, + $readonly, + ); + } +} diff --git a/lib/private/Files/Node/HookConnector.php b/lib/private/Files/Node/HookConnector.php new file mode 100644 index 00000000000..1149951174c --- /dev/null +++ b/lib/private/Files/Node/HookConnector.php @@ -0,0 +1,227 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Files\Node; + +use OC\Files\Filesystem; +use OC\Files\View; +use OCP\EventDispatcher\GenericEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Exceptions\AbortedEventException; +use OCP\Files\Events\Node\BeforeNodeCopiedEvent; +use OCP\Files\Events\Node\BeforeNodeCreatedEvent; +use OCP\Files\Events\Node\BeforeNodeDeletedEvent; +use OCP\Files\Events\Node\BeforeNodeReadEvent; +use OCP\Files\Events\Node\BeforeNodeRenamedEvent; +use OCP\Files\Events\Node\BeforeNodeTouchedEvent; +use OCP\Files\Events\Node\BeforeNodeWrittenEvent; +use OCP\Files\Events\Node\NodeCopiedEvent; +use OCP\Files\Events\Node\NodeCreatedEvent; +use OCP\Files\Events\Node\NodeDeletedEvent; +use OCP\Files\Events\Node\NodeRenamedEvent; +use OCP\Files\Events\Node\NodeTouchedEvent; +use OCP\Files\Events\Node\NodeWrittenEvent; +use OCP\Files\FileInfo; +use OCP\Files\IRootFolder; +use OCP\Util; +use Psr\Log\LoggerInterface; + +class HookConnector { + /** @var FileInfo[] */ + private array $deleteMetaCache = []; + + public function __construct( + private IRootFolder $root, + private View $view, + private IEventDispatcher $dispatcher, + private LoggerInterface $logger, + ) { + } + + public function viewToNode() { + Util::connectHook('OC_Filesystem', 'write', $this, 'write'); + Util::connectHook('OC_Filesystem', 'post_write', $this, 'postWrite'); + + Util::connectHook('OC_Filesystem', 'create', $this, 'create'); + Util::connectHook('OC_Filesystem', 'post_create', $this, 'postCreate'); + + Util::connectHook('OC_Filesystem', 'delete', $this, 'delete'); + Util::connectHook('OC_Filesystem', 'post_delete', $this, 'postDelete'); + + Util::connectHook('OC_Filesystem', 'rename', $this, 'rename'); + Util::connectHook('OC_Filesystem', 'post_rename', $this, 'postRename'); + + Util::connectHook('OC_Filesystem', 'copy', $this, 'copy'); + Util::connectHook('OC_Filesystem', 'post_copy', $this, 'postCopy'); + + Util::connectHook('OC_Filesystem', 'touch', $this, 'touch'); + Util::connectHook('OC_Filesystem', 'post_touch', $this, 'postTouch'); + + Util::connectHook('OC_Filesystem', 'read', $this, 'read'); + } + + public function write($arguments) { + $node = $this->getNodeForPath($arguments['path']); + $this->root->emit('\OC\Files', 'preWrite', [$node]); + $this->dispatcher->dispatch('\OCP\Files::preWrite', new GenericEvent($node)); + + $event = new BeforeNodeWrittenEvent($node); + $this->dispatcher->dispatchTyped($event); + } + + public function postWrite($arguments) { + $node = $this->getNodeForPath($arguments['path']); + $this->root->emit('\OC\Files', 'postWrite', [$node]); + $this->dispatcher->dispatch('\OCP\Files::postWrite', new GenericEvent($node)); + + $event = new NodeWrittenEvent($node); + $this->dispatcher->dispatchTyped($event); + } + + public function create($arguments) { + $node = $this->getNodeForPath($arguments['path']); + $this->root->emit('\OC\Files', 'preCreate', [$node]); + $this->dispatcher->dispatch('\OCP\Files::preCreate', new GenericEvent($node)); + + $event = new BeforeNodeCreatedEvent($node); + $this->dispatcher->dispatchTyped($event); + } + + public function postCreate($arguments) { + $node = $this->getNodeForPath($arguments['path']); + $this->root->emit('\OC\Files', 'postCreate', [$node]); + $this->dispatcher->dispatch('\OCP\Files::postCreate', new GenericEvent($node)); + + $event = new NodeCreatedEvent($node); + $this->dispatcher->dispatchTyped($event); + } + + public function delete($arguments) { + $node = $this->getNodeForPath($arguments['path']); + $this->deleteMetaCache[$node->getPath()] = $node->getFileInfo(); + $this->root->emit('\OC\Files', 'preDelete', [$node]); + $this->dispatcher->dispatch('\OCP\Files::preDelete', new GenericEvent($node)); + + $event = new BeforeNodeDeletedEvent($node); + try { + $this->dispatcher->dispatchTyped($event); + } catch (AbortedEventException $e) { + $arguments['run'] = false; + $this->logger->warning('delete process aborted', ['exception' => $e]); + } + } + + public function postDelete($arguments) { + $node = $this->getNodeForPath($arguments['path']); + unset($this->deleteMetaCache[$node->getPath()]); + $this->root->emit('\OC\Files', 'postDelete', [$node]); + $this->dispatcher->dispatch('\OCP\Files::postDelete', new GenericEvent($node)); + + $event = new NodeDeletedEvent($node); + $this->dispatcher->dispatchTyped($event); + } + + public function touch($arguments) { + $node = $this->getNodeForPath($arguments['path']); + $this->root->emit('\OC\Files', 'preTouch', [$node]); + $this->dispatcher->dispatch('\OCP\Files::preTouch', new GenericEvent($node)); + + $event = new BeforeNodeTouchedEvent($node); + $this->dispatcher->dispatchTyped($event); + } + + public function postTouch($arguments) { + $node = $this->getNodeForPath($arguments['path']); + $this->root->emit('\OC\Files', 'postTouch', [$node]); + $this->dispatcher->dispatch('\OCP\Files::postTouch', new GenericEvent($node)); + + $event = new NodeTouchedEvent($node); + $this->dispatcher->dispatchTyped($event); + } + + public function rename($arguments) { + $source = $this->getNodeForPath($arguments['oldpath']); + $target = $this->getNodeForPath($arguments['newpath']); + $this->root->emit('\OC\Files', 'preRename', [$source, $target]); + $this->dispatcher->dispatch('\OCP\Files::preRename', new GenericEvent([$source, $target])); + + $event = new BeforeNodeRenamedEvent($source, $target); + try { + $this->dispatcher->dispatchTyped($event); + } catch (AbortedEventException $e) { + $arguments['run'] = false; + $this->logger->warning('rename process aborted', ['exception' => $e]); + } + } + + public function postRename($arguments) { + $source = $this->getNodeForPath($arguments['oldpath']); + $target = $this->getNodeForPath($arguments['newpath']); + $this->root->emit('\OC\Files', 'postRename', [$source, $target]); + $this->dispatcher->dispatch('\OCP\Files::postRename', new GenericEvent([$source, $target])); + + $event = new NodeRenamedEvent($source, $target); + $this->dispatcher->dispatchTyped($event); + } + + public function copy($arguments) { + $source = $this->getNodeForPath($arguments['oldpath']); + $target = $this->getNodeForPath($arguments['newpath'], $source instanceof Folder); + $this->root->emit('\OC\Files', 'preCopy', [$source, $target]); + $this->dispatcher->dispatch('\OCP\Files::preCopy', new GenericEvent([$source, $target])); + + $event = new BeforeNodeCopiedEvent($source, $target); + try { + $this->dispatcher->dispatchTyped($event); + } catch (AbortedEventException $e) { + $arguments['run'] = false; + $this->logger->warning('copy process aborted', ['exception' => $e]); + } + } + + public function postCopy($arguments) { + $source = $this->getNodeForPath($arguments['oldpath']); + $target = $this->getNodeForPath($arguments['newpath']); + $this->root->emit('\OC\Files', 'postCopy', [$source, $target]); + $this->dispatcher->dispatch('\OCP\Files::postCopy', new GenericEvent([$source, $target])); + + $event = new NodeCopiedEvent($source, $target); + $this->dispatcher->dispatchTyped($event); + } + + public function read($arguments) { + $node = $this->getNodeForPath($arguments['path']); + $this->root->emit('\OC\Files', 'read', [$node]); + $this->dispatcher->dispatch('\OCP\Files::read', new GenericEvent([$node])); + + $event = new BeforeNodeReadEvent($node); + $this->dispatcher->dispatchTyped($event); + } + + private function getNodeForPath(string $path, bool $isDir = false): Node { + $info = Filesystem::getView()->getFileInfo($path); + if (!$info) { + $fullPath = Filesystem::getView()->getAbsolutePath($path); + if (isset($this->deleteMetaCache[$fullPath])) { + $info = $this->deleteMetaCache[$fullPath]; + } else { + $info = null; + } + if ($isDir || Filesystem::is_dir($path)) { + return new NonExistingFolder($this->root, $this->view, $fullPath, $info); + } else { + return new NonExistingFile($this->root, $this->view, $fullPath, $info); + } + } + if ($info->getType() === FileInfo::TYPE_FILE) { + return new File($this->root, $this->view, $info->getPath(), $info); + } else { + return new Folder($this->root, $this->view, $info->getPath(), $info); + } + } +} diff --git a/lib/private/Files/Node/LazyFolder.php b/lib/private/Files/Node/LazyFolder.php new file mode 100644 index 00000000000..37b1efa0fad --- /dev/null +++ b/lib/private/Files/Node/LazyFolder.php @@ -0,0 +1,568 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Files\Node; + +use OC\Files\Filesystem; +use OC\Files\Utils\PathHelper; +use OCP\Constants; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\NotPermittedException; + +/** + * Class LazyFolder + * + * This is a lazy wrapper around a folder. So only + * once it is needed this will get initialized. + * + * @package OC\Files\Node + */ +class LazyFolder implements Folder { + /** @var \Closure(): Folder */ + private \Closure $folderClosure; + protected ?Folder $folder = null; + protected IRootFolder $rootFolder; + protected array $data; + + /** + * @param IRootFolder $rootFolder + * @param \Closure(): Folder $folderClosure + * @param array $data + */ + public function __construct(IRootFolder $rootFolder, \Closure $folderClosure, array $data = []) { + $this->rootFolder = $rootFolder; + $this->folderClosure = $folderClosure; + $this->data = $data; + } + + protected function getRootFolder(): IRootFolder { + return $this->rootFolder; + } + + protected function getRealFolder(): Folder { + if ($this->folder === null) { + $this->folder = call_user_func($this->folderClosure); + } + return $this->folder; + } + + /** + * Magic method to first get the real rootFolder and then + * call $method with $args on it + * + * @param $method + * @param $args + * @return mixed + */ + public function __call($method, $args) { + return call_user_func_array([$this->getRealFolder(), $method], $args); + } + + /** + * @inheritDoc + */ + public function getUser() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function listen($scope, $method, callable $callback) { + $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function removeListener($scope = null, $method = null, ?callable $callback = null) { + $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function emit($scope, $method, $arguments = []) { + $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function mount($storage, $mountPoint, $arguments = []) { + $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getMount(string $mountPoint): IMountPoint { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @return IMountPoint[] + */ + public function getMountsIn(string $mountPoint): array { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getMountByStorageId($storageId) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getMountByNumericStorageId($numericId) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function unMount($mount) { + $this->__call(__FUNCTION__, func_get_args()); + } + + public function get($path) { + return $this->getRootFolder()->get($this->getFullPath($path)); + } + + /** + * @inheritDoc + */ + public function rename($targetPath) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function delete() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function copy($targetPath) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function touch($mtime = null) { + $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getStorage() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getPath() { + if (isset($this->data['path'])) { + return $this->data['path']; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getInternalPath() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getId() { + if (isset($this->data['fileid'])) { + return $this->data['fileid']; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function stat() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getMTime() { + if (isset($this->data['mtime'])) { + return $this->data['mtime']; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getSize($includeMounts = true): int|float { + if (isset($this->data['size'])) { + return $this->data['size']; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getEtag() { + if (isset($this->data['etag'])) { + return $this->data['etag']; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getPermissions() { + if (isset($this->data['permissions'])) { + return $this->data['permissions']; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function isReadable() { + if (isset($this->data['permissions'])) { + return ($this->data['permissions'] & Constants::PERMISSION_READ) == Constants::PERMISSION_READ; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function isUpdateable() { + if (isset($this->data['permissions'])) { + return ($this->data['permissions'] & Constants::PERMISSION_UPDATE) == Constants::PERMISSION_UPDATE; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function isDeletable() { + if (isset($this->data['permissions'])) { + return ($this->data['permissions'] & Constants::PERMISSION_DELETE) == Constants::PERMISSION_DELETE; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function isShareable() { + if (isset($this->data['permissions'])) { + return ($this->data['permissions'] & Constants::PERMISSION_SHARE) == Constants::PERMISSION_SHARE; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getParent() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getName() { + if (isset($this->data['path'])) { + return basename($this->data['path']); + } + if (isset($this->data['name'])) { + return $this->data['name']; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getUserFolder($userId) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getMimetype() { + if (isset($this->data['mimetype'])) { + return $this->data['mimetype']; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getMimePart() { + if (isset($this->data['mimetype'])) { + [$part,] = explode('/', $this->data['mimetype']); + return $part; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function isEncrypted() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getType() { + if (isset($this->data['type'])) { + return $this->data['type']; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function isShared() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function isMounted() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getMountPoint() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getOwner() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getChecksum() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + public function getExtension(): string { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getFullPath($path) { + if (isset($this->data['path'])) { + $path = PathHelper::normalizePath($path); + if (!Filesystem::isValidPath($path)) { + throw new NotPermittedException('Invalid path "' . $path . '"'); + } + return $this->data['path'] . $path; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function isSubNode($node) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getDirectoryListing() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + public function nodeExists($path) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function newFolder($path) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function newFile($path, $content = null) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function search($query) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function searchByMime($mimetype) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function searchByTag($tag, $userId) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + public function searchBySystemTag(string $tagName, string $userId, int $limit = 0, int $offset = 0) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getById($id) { + return $this->getRootFolder()->getByIdInPath((int)$id, $this->getPath()); + } + + public function getFirstNodeById(int $id): ?\OCP\Files\Node { + return $this->getRootFolder()->getFirstNodeByIdInPath($id, $this->getPath()); + } + + /** + * @inheritDoc + */ + public function getFreeSpace() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function isCreatable() { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getNonExistingName($name) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function move($targetPath) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function lock($type) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function changeLock($targetType) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function unlock($type) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getRecent($limit, $offset = 0) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getCreationTime(): int { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + */ + public function getUploadTime(): int { + return $this->__call(__FUNCTION__, func_get_args()); + } + + public function getRelativePath($path) { + return PathHelper::getRelativePath($this->getPath(), $path); + } + + public function getParentId(): int { + if (isset($this->data['parent'])) { + return $this->data['parent']; + } + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + * @return array<string, int|string|bool|float|string[]|int[]> + */ + public function getMetadata(): array { + return $this->data['metadata'] ?? $this->__call(__FUNCTION__, func_get_args()); + } + + public function verifyPath($fileName, $readonly = false): void { + $this->__call(__FUNCTION__, func_get_args()); + } +} diff --git a/lib/private/Files/Node/LazyRoot.php b/lib/private/Files/Node/LazyRoot.php new file mode 100644 index 00000000000..bc3f3a2e80f --- /dev/null +++ b/lib/private/Files/Node/LazyRoot.php @@ -0,0 +1,56 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Files\Node; + +use OCP\Files\Cache\ICacheEntry; +use OCP\Files\IRootFolder; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\Node; +use OCP\Files\Node as INode; + +/** + * Class LazyRoot + * + * This is a lazy wrapper around the root. So only + * once it is needed this will get initialized. + * + * @package OC\Files\Node + */ +class LazyRoot extends LazyFolder implements IRootFolder { + public function __construct(\Closure $folderClosure, array $data = []) { + parent::__construct($this, $folderClosure, $data); + } + + protected function getRootFolder(): IRootFolder { + $folder = $this->getRealFolder(); + if (!$folder instanceof IRootFolder) { + throw new \Exception('Lazy root folder closure didn\'t return a root folder'); + } + return $folder; + } + + public function getUserFolder($userId) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + public function getByIdInPath(int $id, string $path) { + return $this->__call(__FUNCTION__, func_get_args()); + } + + public function getFirstNodeByIdInPath(int $id, string $path): ?Node { + return $this->__call(__FUNCTION__, func_get_args()); + } + + public function getNodeFromCacheEntryAndMount(ICacheEntry $cacheEntry, IMountPoint $mountPoint): INode { + return $this->getRootFolder()->getNodeFromCacheEntryAndMount($cacheEntry, $mountPoint); + } + + public function getAppDataDirectoryName(): string { + return $this->__call(__FUNCTION__, func_get_args()); + } +} diff --git a/lib/private/Files/Node/LazyUserFolder.php b/lib/private/Files/Node/LazyUserFolder.php new file mode 100644 index 00000000000..77479c2fa5e --- /dev/null +++ b/lib/private/Files/Node/LazyUserFolder.php @@ -0,0 +1,66 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Files\Node; + +use OCP\Constants; +use OCP\Files\File; +use OCP\Files\FileInfo; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\Mount\IMountManager; +use OCP\Files\NotFoundException; +use OCP\IUser; +use Psr\Log\LoggerInterface; + +class LazyUserFolder extends LazyFolder { + private IUser $user; + private string $path; + private IMountManager $mountManager; + + public function __construct(IRootFolder $rootFolder, IUser $user, IMountManager $mountManager) { + $this->user = $user; + $this->mountManager = $mountManager; + $this->path = '/' . $user->getUID() . '/files'; + parent::__construct($rootFolder, function () use ($user): Folder { + try { + $node = $this->getRootFolder()->get($this->path); + if ($node instanceof File) { + $e = new \RuntimeException(); + \OCP\Server::get(LoggerInterface::class)->error('User root storage is not a folder: ' . $this->path, [ + 'exception' => $e, + ]); + throw $e; + } + return $node; + } catch (NotFoundException $e) { + if (!$this->getRootFolder()->nodeExists('/' . $user->getUID())) { + $this->getRootFolder()->newFolder('/' . $user->getUID()); + } + return $this->getRootFolder()->newFolder($this->path); + } + }, [ + 'path' => $this->path, + // Sharing user root folder is not allowed + 'permissions' => Constants::PERMISSION_ALL ^ Constants::PERMISSION_SHARE, + 'type' => FileInfo::TYPE_FOLDER, + 'mimetype' => FileInfo::MIMETYPE_FOLDER, + ]); + } + + public function getMountPoint() { + if ($this->folder !== null) { + return $this->folder->getMountPoint(); + } + $mountPoint = $this->mountManager->find('/' . $this->user->getUID()); + if (is_null($mountPoint)) { + throw new \Exception('No mountpoint for user folder'); + } + return $mountPoint; + } +} diff --git a/lib/private/Files/Node/Node.php b/lib/private/Files/Node/Node.php new file mode 100644 index 00000000000..5dbdc4054bf --- /dev/null +++ b/lib/private/Files/Node/Node.php @@ -0,0 +1,489 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Files\Node; + +use OC\Files\Filesystem; +use OC\Files\Mount\MoveableMount; +use OC\Files\Utils\PathHelper; +use OCP\EventDispatcher\GenericEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\FileInfo; +use OCP\Files\InvalidPathException; +use OCP\Files\IRootFolder; +use OCP\Files\Node as INode; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\Lock\LockedException; +use OCP\PreConditionNotMetException; + +// FIXME: this class really should be abstract (+1) +class Node implements INode { + /** + * @var \OC\Files\View $view + */ + protected $view; + + protected IRootFolder $root; + + /** + * @var string $path Absolute path to the node (e.g. /admin/files/folder/file) + */ + protected $path; + + protected ?FileInfo $fileInfo; + + protected ?INode $parent; + + private bool $infoHasSubMountsIncluded; + + /** + * @param \OC\Files\View $view + * @param \OCP\Files\IRootFolder $root + * @param string $path + * @param FileInfo $fileInfo + */ + public function __construct(IRootFolder $root, $view, $path, $fileInfo = null, ?INode $parent = null, bool $infoHasSubMountsIncluded = true) { + if (Filesystem::normalizePath($view->getRoot()) !== '/') { + throw new PreConditionNotMetException('The view passed to the node should not have any fake root set'); + } + $this->view = $view; + $this->root = $root; + $this->path = $path; + $this->fileInfo = $fileInfo; + $this->parent = $parent; + $this->infoHasSubMountsIncluded = $infoHasSubMountsIncluded; + } + + /** + * Creates a Node of the same type that represents a non-existing path + * + * @param string $path path + * @return Node non-existing node + * @throws \Exception + */ + protected function createNonExistingNode($path) { + throw new \Exception('Must be implemented by subclasses'); + } + + /** + * Returns the matching file info + * + * @return FileInfo + * @throws InvalidPathException + * @throws NotFoundException + */ + public function getFileInfo(bool $includeMountPoint = true) { + if (!$this->fileInfo) { + if (!Filesystem::isValidPath($this->path)) { + throw new InvalidPathException(); + } + $fileInfo = $this->view->getFileInfo($this->path, $includeMountPoint); + $this->infoHasSubMountsIncluded = $includeMountPoint; + if ($fileInfo instanceof FileInfo) { + $this->fileInfo = $fileInfo; + } else { + throw new NotFoundException(); + } + } elseif ($includeMountPoint && !$this->infoHasSubMountsIncluded && $this instanceof Folder) { + if ($this->fileInfo instanceof \OC\Files\FileInfo) { + $this->view->addSubMounts($this->fileInfo); + } + $this->infoHasSubMountsIncluded = true; + } + return $this->fileInfo; + } + + /** + * @param string[] $hooks + */ + protected function sendHooks($hooks, ?array $args = null) { + $args = !empty($args) ? $args : [$this]; + /** @var IEventDispatcher $dispatcher */ + $dispatcher = \OC::$server->get(IEventDispatcher::class); + foreach ($hooks as $hook) { + if (method_exists($this->root, 'emit')) { + $this->root->emit('\OC\Files', $hook, $args); + } + + if (in_array($hook, ['preWrite', 'postWrite', 'preCreate', 'postCreate', 'preTouch', 'postTouch', 'preDelete', 'postDelete'], true)) { + $event = new GenericEvent($args[0]); + } else { + $event = new GenericEvent($args); + } + + $dispatcher->dispatch('\OCP\Files::' . $hook, $event); + } + } + + /** + * @param int $permissions + * @return bool + * @throws InvalidPathException + * @throws NotFoundException + */ + protected function checkPermissions($permissions) { + return ($this->getPermissions() & $permissions) === $permissions; + } + + public function delete() { + } + + /** + * @param int $mtime + * @throws InvalidPathException + * @throws NotFoundException + * @throws NotPermittedException + */ + public function touch($mtime = null) { + if ($this->checkPermissions(\OCP\Constants::PERMISSION_UPDATE)) { + $this->sendHooks(['preTouch']); + $this->view->touch($this->path, $mtime); + $this->sendHooks(['postTouch']); + if ($this->fileInfo) { + if (is_null($mtime)) { + $mtime = time(); + } + $this->fileInfo['mtime'] = $mtime; + } + } else { + throw new NotPermittedException(); + } + } + + public function getStorage() { + $storage = $this->getMountPoint()->getStorage(); + if (!$storage) { + throw new \Exception('No storage for node'); + } + return $storage; + } + + /** + * @return string + */ + public function getPath() { + return $this->path; + } + + /** + * @return string + */ + public function getInternalPath() { + return $this->getFileInfo(false)->getInternalPath(); + } + + /** + * @return int + * @throws InvalidPathException + * @throws NotFoundException + */ + public function getId() { + return $this->getFileInfo(false)->getId() ?? -1; + } + + /** + * @return array + */ + public function stat() { + return $this->view->stat($this->path); + } + + /** + * @return int + * @throws InvalidPathException + * @throws NotFoundException + */ + public function getMTime() { + return $this->getFileInfo()->getMTime(); + } + + /** + * @param bool $includeMounts + * @return int|float + * @throws InvalidPathException + * @throws NotFoundException + */ + public function getSize($includeMounts = true): int|float { + return $this->getFileInfo()->getSize($includeMounts); + } + + /** + * @return string + * @throws InvalidPathException + * @throws NotFoundException + */ + public function getEtag() { + return $this->getFileInfo()->getEtag(); + } + + /** + * @return int + * @throws InvalidPathException + * @throws NotFoundException + */ + public function getPermissions() { + return $this->getFileInfo(false)->getPermissions(); + } + + /** + * @return bool + * @throws InvalidPathException + * @throws NotFoundException + */ + public function isReadable() { + return $this->getFileInfo(false)->isReadable(); + } + + /** + * @return bool + * @throws InvalidPathException + * @throws NotFoundException + */ + public function isUpdateable() { + return $this->getFileInfo(false)->isUpdateable(); + } + + /** + * @return bool + * @throws InvalidPathException + * @throws NotFoundException + */ + public function isDeletable() { + return $this->getFileInfo(false)->isDeletable(); + } + + /** + * @return bool + * @throws InvalidPathException + * @throws NotFoundException + */ + public function isShareable() { + return $this->getFileInfo(false)->isShareable(); + } + + /** + * @return bool + * @throws InvalidPathException + * @throws NotFoundException + */ + public function isCreatable() { + return $this->getFileInfo(false)->isCreatable(); + } + + public function getParent(): INode|IRootFolder { + if ($this->parent === null) { + $newPath = dirname($this->path); + if ($newPath === '' || $newPath === '.' || $newPath === '/') { + return $this->root; + } + + // Manually fetch the parent if the current node doesn't have a file info yet + try { + $fileInfo = $this->getFileInfo(); + } catch (NotFoundException) { + $this->parent = $this->root->get($newPath); + /** @var \OCP\Files\Folder $this->parent */ + return $this->parent; + } + + // gather the metadata we already know about our parent + $parentData = [ + 'path' => $newPath, + 'fileid' => $fileInfo->getParentId(), + ]; + + // and create lazy folder with it instead of always querying + $this->parent = new LazyFolder($this->root, function () use ($newPath) { + return $this->root->get($newPath); + }, $parentData); + } + + return $this->parent; + } + + /** + * @return string + */ + public function getName() { + return basename($this->path); + } + + /** + * @param string $path + * @return string + */ + protected function normalizePath($path) { + return PathHelper::normalizePath($path); + } + + /** + * check if the requested path is valid + * + * @param string $path + * @return bool + */ + public function isValidPath($path) { + return Filesystem::isValidPath($path); + } + + public function isMounted() { + return $this->getFileInfo(false)->isMounted(); + } + + public function isShared() { + return $this->getFileInfo(false)->isShared(); + } + + public function getMimeType() { + return $this->getFileInfo(false)->getMimetype(); + } + + public function getMimePart() { + return $this->getFileInfo(false)->getMimePart(); + } + + public function getType() { + return $this->getFileInfo(false)->getType(); + } + + public function isEncrypted() { + return $this->getFileInfo(false)->isEncrypted(); + } + + public function getMountPoint() { + return $this->getFileInfo(false)->getMountPoint(); + } + + public function getOwner() { + return $this->getFileInfo(false)->getOwner(); + } + + public function getChecksum() { + } + + public function getExtension(): string { + return $this->getFileInfo(false)->getExtension(); + } + + /** + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @throws LockedException + */ + public function lock($type) { + $this->view->lockFile($this->path, $type); + } + + /** + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @throws LockedException + */ + public function changeLock($type) { + $this->view->changeLock($this->path, $type); + } + + /** + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @throws LockedException + */ + public function unlock($type) { + $this->view->unlockFile($this->path, $type); + } + + /** + * @param string $targetPath + * @return INode + * @throws InvalidPathException + * @throws NotFoundException + * @throws NotPermittedException if copy not allowed or failed + */ + public function copy($targetPath) { + $targetPath = $this->normalizePath($targetPath); + $parent = $this->root->get(dirname($targetPath)); + if ($parent instanceof Folder and $this->isValidPath($targetPath) and $parent->isCreatable()) { + $nonExisting = $this->createNonExistingNode($targetPath); + $this->sendHooks(['preCopy'], [$this, $nonExisting]); + $this->sendHooks(['preWrite'], [$nonExisting]); + if (!$this->view->copy($this->path, $targetPath)) { + throw new NotPermittedException('Could not copy ' . $this->path . ' to ' . $targetPath); + } + $targetNode = $this->root->get($targetPath); + $this->sendHooks(['postCopy'], [$this, $targetNode]); + $this->sendHooks(['postWrite'], [$targetNode]); + return $targetNode; + } else { + throw new NotPermittedException('No permission to copy to path ' . $targetPath); + } + } + + /** + * @param string $targetPath + * @return INode + * @throws InvalidPathException + * @throws NotFoundException + * @throws NotPermittedException if move not allowed or failed + * @throws LockedException + */ + public function move($targetPath) { + $targetPath = $this->normalizePath($targetPath); + $parent = $this->root->get(dirname($targetPath)); + if ( + ($parent instanceof Folder) + && $this->isValidPath($targetPath) + && ( + $parent->isCreatable() + || ( + $parent->getInternalPath() === '' + && ($parent->getMountPoint() instanceof MoveableMount) + ) + ) + ) { + $nonExisting = $this->createNonExistingNode($targetPath); + $this->sendHooks(['preRename'], [$this, $nonExisting]); + $this->sendHooks(['preWrite'], [$nonExisting]); + if (!$this->view->rename($this->path, $targetPath)) { + throw new NotPermittedException('Could not move ' . $this->path . ' to ' . $targetPath); + } + + $mountPoint = $this->getMountPoint(); + if ($mountPoint) { + // update the cached fileinfo with the new (internal) path + /** @var \OC\Files\FileInfo $oldFileInfo */ + $oldFileInfo = $this->getFileInfo(); + $this->fileInfo = new \OC\Files\FileInfo($targetPath, $oldFileInfo->getStorage(), $mountPoint->getInternalPath($targetPath), $oldFileInfo->getData(), $mountPoint, $oldFileInfo->getOwner()); + } + + $targetNode = $this->root->get($targetPath); + $this->sendHooks(['postRename'], [$this, $targetNode]); + $this->sendHooks(['postWrite'], [$targetNode]); + $this->path = $targetPath; + return $targetNode; + } else { + throw new NotPermittedException('No permission to move to path ' . $targetPath); + } + } + + public function getCreationTime(): int { + return $this->getFileInfo()->getCreationTime(); + } + + public function getUploadTime(): int { + return $this->getFileInfo()->getUploadTime(); + } + + public function getParentId(): int { + return $this->fileInfo->getParentId(); + } + + /** + * @inheritDoc + * @return array<string, int|string|bool|float|string[]|int[]> + */ + public function getMetadata(): array { + return $this->fileInfo->getMetadata(); + } +} diff --git a/lib/private/Files/Node/NonExistingFile.php b/lib/private/Files/Node/NonExistingFile.php new file mode 100644 index 00000000000..66ec2e6c040 --- /dev/null +++ b/lib/private/Files/Node/NonExistingFile.php @@ -0,0 +1,136 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Files\Node; + +use OCP\Files\NotFoundException; + +class NonExistingFile extends File { + /** + * @param string $newPath + * @throws \OCP\Files\NotFoundException + */ + public function rename($newPath) { + throw new NotFoundException(); + } + + public function delete() { + throw new NotFoundException(); + } + + public function copy($targetPath) { + throw new NotFoundException(); + } + + public function touch($mtime = null) { + throw new NotFoundException(); + } + + public function getId() { + if ($this->fileInfo) { + return parent::getId(); + } else { + throw new NotFoundException(); + } + } + + public function getInternalPath() { + if ($this->fileInfo) { + return parent::getInternalPath(); + } else { + return $this->getParent()->getMountPoint()->getInternalPath($this->getPath()); + } + } + + public function stat() { + throw new NotFoundException(); + } + + public function getMTime() { + if ($this->fileInfo) { + return parent::getMTime(); + } else { + throw new NotFoundException(); + } + } + + public function getSize($includeMounts = true): int|float { + if ($this->fileInfo) { + return parent::getSize($includeMounts); + } else { + throw new NotFoundException(); + } + } + + public function getEtag() { + if ($this->fileInfo) { + return parent::getEtag(); + } else { + throw new NotFoundException(); + } + } + + public function getPermissions() { + if ($this->fileInfo) { + return parent::getPermissions(); + } else { + throw new NotFoundException(); + } + } + + public function isReadable() { + if ($this->fileInfo) { + return parent::isReadable(); + } else { + throw new NotFoundException(); + } + } + + public function isUpdateable() { + if ($this->fileInfo) { + return parent::isUpdateable(); + } else { + throw new NotFoundException(); + } + } + + public function isDeletable() { + if ($this->fileInfo) { + return parent::isDeletable(); + } else { + throw new NotFoundException(); + } + } + + public function isShareable() { + if ($this->fileInfo) { + return parent::isShareable(); + } else { + throw new NotFoundException(); + } + } + + public function getContent() { + throw new NotFoundException(); + } + + public function putContent($data) { + throw new NotFoundException(); + } + + public function getMimeType() { + if ($this->fileInfo) { + return parent::getMimeType(); + } else { + throw new NotFoundException(); + } + } + + public function fopen($mode) { + throw new NotFoundException(); + } +} diff --git a/lib/private/Files/Node/NonExistingFolder.php b/lib/private/Files/Node/NonExistingFolder.php new file mode 100644 index 00000000000..4489fdaf010 --- /dev/null +++ b/lib/private/Files/Node/NonExistingFolder.php @@ -0,0 +1,172 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Files\Node; + +use OCP\Files\NotFoundException; + +class NonExistingFolder extends Folder { + /** + * @param string $newPath + * @throws \OCP\Files\NotFoundException + */ + public function rename($newPath) { + throw new NotFoundException(); + } + + public function delete() { + throw new NotFoundException(); + } + + public function copy($targetPath) { + throw new NotFoundException(); + } + + public function touch($mtime = null) { + throw new NotFoundException(); + } + + public function getId() { + if ($this->fileInfo) { + return parent::getId(); + } else { + throw new NotFoundException(); + } + } + + public function getInternalPath() { + if ($this->fileInfo) { + return parent::getInternalPath(); + } else { + return $this->getParent()->getMountPoint()->getInternalPath($this->getPath()); + } + } + + public function stat() { + throw new NotFoundException(); + } + + public function getMTime() { + if ($this->fileInfo) { + return parent::getMTime(); + } else { + throw new NotFoundException(); + } + } + + public function getSize($includeMounts = true): int|float { + if ($this->fileInfo) { + return parent::getSize($includeMounts); + } else { + throw new NotFoundException(); + } + } + + public function getEtag() { + if ($this->fileInfo) { + return parent::getEtag(); + } else { + throw new NotFoundException(); + } + } + + public function getPermissions() { + if ($this->fileInfo) { + return parent::getPermissions(); + } else { + throw new NotFoundException(); + } + } + + public function isReadable() { + if ($this->fileInfo) { + return parent::isReadable(); + } else { + throw new NotFoundException(); + } + } + + public function isUpdateable() { + if ($this->fileInfo) { + return parent::isUpdateable(); + } else { + throw new NotFoundException(); + } + } + + public function isDeletable() { + if ($this->fileInfo) { + return parent::isDeletable(); + } else { + throw new NotFoundException(); + } + } + + public function isShareable() { + if ($this->fileInfo) { + return parent::isShareable(); + } else { + throw new NotFoundException(); + } + } + + public function get($path) { + throw new NotFoundException(); + } + + public function getDirectoryListing() { + throw new NotFoundException(); + } + + public function nodeExists($path) { + return false; + } + + public function newFolder($path) { + throw new NotFoundException(); + } + + public function newFile($path, $content = null) { + throw new NotFoundException(); + } + + public function search($query) { + throw new NotFoundException(); + } + + public function searchByMime($mimetype) { + throw new NotFoundException(); + } + + public function searchByTag($tag, $userId) { + throw new NotFoundException(); + } + + public function searchBySystemTag(string $tagName, string $userId, int $limit = 0, int $offset = 0): array { + throw new NotFoundException(); + } + + public function getById($id) { + throw new NotFoundException(); + } + + public function getFirstNodeById(int $id): ?\OCP\Files\Node { + throw new NotFoundException(); + } + + public function getFreeSpace() { + throw new NotFoundException(); + } + + public function isCreatable() { + if ($this->fileInfo) { + return parent::isCreatable(); + } else { + throw new NotFoundException(); + } + } +} diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php new file mode 100644 index 00000000000..76afca9dee8 --- /dev/null +++ b/lib/private/Files/Node/Root.php @@ -0,0 +1,534 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Files\Node; + +use OC\Files\FileInfo; +use OC\Files\Mount\Manager; +use OC\Files\Mount\MountPoint; +use OC\Files\Utils\PathHelper; +use OC\Files\View; +use OC\Hooks\PublicEmitter; +use OC\User\NoUserException; +use OCP\Cache\CappedMemoryCache; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Config\IUserMountCache; +use OCP\Files\Events\Node\FilesystemTornDownEvent; +use OCP\Files\IRootFolder; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\Node as INode; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Server; +use Psr\Log\LoggerInterface; + +/** + * Class Root + * + * Hooks available in scope \OC\Files + * - preWrite(\OCP\Files\Node $node) + * - postWrite(\OCP\Files\Node $node) + * - preCreate(\OCP\Files\Node $node) + * - postCreate(\OCP\Files\Node $node) + * - preDelete(\OCP\Files\Node $node) + * - postDelete(\OCP\Files\Node $node) + * - preTouch(\OC\FilesP\Node $node, int $mtime) + * - postTouch(\OCP\Files\Node $node) + * - preCopy(\OCP\Files\Node $source, \OCP\Files\Node $target) + * - postCopy(\OCP\Files\Node $source, \OCP\Files\Node $target) + * - preRename(\OCP\Files\Node $source, \OCP\Files\Node $target) + * - postRename(\OCP\Files\Node $source, \OCP\Files\Node $target) + * + * @package OC\Files\Node + */ +class Root extends Folder implements IRootFolder { + private Manager $mountManager; + private PublicEmitter $emitter; + private ?IUser $user; + private CappedMemoryCache $userFolderCache; + private IUserMountCache $userMountCache; + private LoggerInterface $logger; + private IUserManager $userManager; + private IEventDispatcher $eventDispatcher; + private ICache $pathByIdCache; + + /** + * @param Manager $manager + * @param View $view + * @param IUser|null $user + */ + public function __construct( + $manager, + $view, + $user, + IUserMountCache $userMountCache, + LoggerInterface $logger, + IUserManager $userManager, + IEventDispatcher $eventDispatcher, + ICacheFactory $cacheFactory, + ) { + parent::__construct($this, $view, ''); + $this->mountManager = $manager; + $this->user = $user; + $this->emitter = new PublicEmitter(); + $this->userFolderCache = new CappedMemoryCache(); + $this->userMountCache = $userMountCache; + $this->logger = $logger; + $this->userManager = $userManager; + $eventDispatcher->addListener(FilesystemTornDownEvent::class, function () { + $this->userFolderCache = new CappedMemoryCache(); + }); + $this->pathByIdCache = $cacheFactory->createLocal('path-by-id'); + } + + /** + * Get the user for which the filesystem is setup + * + * @return \OC\User\User + */ + public function getUser() { + return $this->user; + } + + /** + * @param string $scope + * @param string $method + * @param callable $callback + */ + public function listen($scope, $method, callable $callback) { + $this->emitter->listen($scope, $method, $callback); + } + + /** + * @param string $scope optional + * @param string $method optional + * @param callable $callback optional + */ + public function removeListener($scope = null, $method = null, ?callable $callback = null) { + $this->emitter->removeListener($scope, $method, $callback); + } + + /** + * @param string $scope + * @param string $method + * @param Node[] $arguments + */ + public function emit($scope, $method, $arguments = []) { + $this->emitter->emit($scope, $method, $arguments); + } + + /** + * @param \OC\Files\Storage\Storage $storage + * @param string $mountPoint + * @param array $arguments + */ + public function mount($storage, $mountPoint, $arguments = []) { + $mount = new MountPoint($storage, $mountPoint, $arguments); + $this->mountManager->addMount($mount); + } + + public function getMount(string $mountPoint): IMountPoint { + return $this->mountManager->find($mountPoint); + } + + /** + * @param string $mountPoint + * @return \OC\Files\Mount\MountPoint[] + */ + public function getMountsIn(string $mountPoint): array { + return $this->mountManager->findIn($mountPoint); + } + + /** + * @param string $storageId + * @return \OC\Files\Mount\MountPoint[] + */ + public function getMountByStorageId($storageId) { + return $this->mountManager->findByStorageId($storageId); + } + + /** + * @param int $numericId + * @return MountPoint[] + */ + public function getMountByNumericStorageId($numericId) { + return $this->mountManager->findByNumericId($numericId); + } + + /** + * @param \OC\Files\Mount\MountPoint $mount + */ + public function unMount($mount) { + $this->mountManager->remove($mount); + } + + public function get($path) { + $path = $this->normalizePath($path); + if ($this->isValidPath($path)) { + $fullPath = $this->getFullPath($path); + $fileInfo = $this->view->getFileInfo($fullPath, false); + if ($fileInfo) { + return $this->createNode($fullPath, $fileInfo, false); + } else { + throw new NotFoundException($path); + } + } else { + throw new NotPermittedException(); + } + } + + //most operations can't be done on the root + + /** + * @param string $targetPath + * @return Node + * @throws \OCP\Files\NotPermittedException + */ + public function rename($targetPath) { + throw new NotPermittedException(); + } + + public function delete() { + throw new NotPermittedException(); + } + + /** + * @param string $targetPath + * @return Node + * @throws \OCP\Files\NotPermittedException + */ + public function copy($targetPath) { + throw new NotPermittedException(); + } + + /** + * @param int $mtime + * @throws \OCP\Files\NotPermittedException + */ + public function touch($mtime = null) { + throw new NotPermittedException(); + } + + /** + * @return \OC\Files\Storage\Storage + * @throws \OCP\Files\NotFoundException + */ + public function getStorage() { + throw new NotFoundException(); + } + + /** + * @return string + */ + public function getPath() { + return '/'; + } + + /** + * @return string + */ + public function getInternalPath() { + return ''; + } + + /** + * @return int + */ + public function getId() { + return 0; + } + + /** + * @return array + */ + public function stat() { + return []; + } + + /** + * @return int + */ + public function getMTime() { + return 0; + } + + /** + * @param bool $includeMounts + * @return int|float + */ + public function getSize($includeMounts = true): int|float { + return 0; + } + + /** + * @return string + */ + public function getEtag() { + return ''; + } + + /** + * @return int + */ + public function getPermissions() { + return \OCP\Constants::PERMISSION_CREATE; + } + + /** + * @return bool + */ + public function isReadable() { + return false; + } + + /** + * @return bool + */ + public function isUpdateable() { + return false; + } + + /** + * @return bool + */ + public function isDeletable() { + return false; + } + + /** + * @return bool + */ + public function isShareable() { + return false; + } + + /** + * @throws \OCP\Files\NotFoundException + */ + public function getParent(): INode|IRootFolder { + throw new NotFoundException(); + } + + /** + * @return string + */ + public function getName() { + return ''; + } + + /** + * Returns a view to user's files folder + * + * @param string $userId user ID + * @return \OCP\Files\Folder + * @throws NoUserException + * @throws NotPermittedException + */ + public function getUserFolder($userId) { + $userObject = $this->userManager->get($userId); + + if (is_null($userObject)) { + $e = new NoUserException('Backends provided no user object'); + $this->logger->error( + sprintf( + 'Backends provided no user object for %s', + $userId + ), + [ + 'app' => 'files', + 'exception' => $e, + ] + ); + throw $e; + } + + $userId = $userObject->getUID(); + + if (!$this->userFolderCache->hasKey($userId)) { + if ($this->mountManager->getSetupManager()->isSetupComplete($userObject)) { + try { + $folder = $this->get('/' . $userId . '/files'); + if (!$folder instanceof \OCP\Files\Folder) { + throw new \Exception("Account folder for \"$userId\" exists as a file"); + } + } catch (NotFoundException $e) { + if (!$this->nodeExists('/' . $userId)) { + $this->newFolder('/' . $userId); + } + $folder = $this->newFolder('/' . $userId . '/files'); + } + } else { + $folder = new LazyUserFolder($this, $userObject, $this->mountManager); + } + + $this->userFolderCache->set($userId, $folder); + } + + return $this->userFolderCache->get($userId); + } + + public function getUserMountCache() { + return $this->userMountCache; + } + + public function getFirstNodeByIdInPath(int $id, string $path): ?INode { + // scope the cache by user, so we don't return nodes for different users + if ($this->user) { + $cachedPath = $this->pathByIdCache->get($this->user->getUID() . '::' . $id); + if ($cachedPath && str_starts_with($cachedPath, $path)) { + // getting the node by path is significantly cheaper than finding it by id + try { + $node = $this->get($cachedPath); + // by validating that the cached path still has the requested fileid we can work around the need to invalidate the cached path + // if the cached path is invalid or a different file now we fall back to the uncached logic + if ($node && $node->getId() === $id) { + return $node; + } + } catch (NotFoundException|NotPermittedException) { + // The file may be moved but the old path still in cache + } + } + } + $node = current($this->getByIdInPath($id, $path)); + if (!$node) { + return null; + } + + if ($this->user) { + $this->pathByIdCache->set($this->user->getUID() . '::' . $id, $node->getPath()); + } + return $node; + } + + /** + * @param int $id + * @return Node[] + */ + public function getByIdInPath(int $id, string $path): array { + $mountCache = $this->getUserMountCache(); + if ($path !== '' && strpos($path, '/', 1) > 0) { + [, $user] = explode('/', $path); + } else { + $user = null; + } + $mountsContainingFile = $mountCache->getMountsForFileId($id, $user); + + // if the mount isn't in the cache yet, perform a setup first, then try again + if (count($mountsContainingFile) === 0) { + $this->mountManager->getSetupManager()->setupForPath($path, true); + $mountsContainingFile = $mountCache->getMountsForFileId($id, $user); + } + + // when a user has access through the same storage through multiple paths + // (such as an external storage that is both mounted for a user and shared to the user) + // the mount cache will only hold a single entry for the storage + // this can lead to issues as the different ways the user has access to a storage can have different permissions + // + // so instead of using the cached entries directly, we instead filter the current mounts by the rootid of the cache entry + + $mountRootIds = array_map(function ($mount) { + return $mount->getRootId(); + }, $mountsContainingFile); + $mountRootPaths = array_map(function ($mount) { + return $mount->getRootInternalPath(); + }, $mountsContainingFile); + $mountProviders = array_unique(array_map(function ($mount) { + return $mount->getMountProvider(); + }, $mountsContainingFile)); + $mountRoots = array_combine($mountRootIds, $mountRootPaths); + + $mounts = $this->mountManager->getMountsByMountProvider($path, $mountProviders); + + $mountsContainingFile = array_filter($mounts, function ($mount) use ($mountRoots) { + return isset($mountRoots[$mount->getStorageRootId()]); + }); + + if (count($mountsContainingFile) === 0) { + if ($user === $this->getAppDataDirectoryName()) { + $folder = $this->get($path); + if ($folder instanceof Folder) { + return $folder->getByIdInRootMount($id); + } else { + throw new \Exception('getByIdInPath with non folder'); + } + } + return []; + } + + $nodes = array_map(function (IMountPoint $mount) use ($id, $mountRoots) { + $rootInternalPath = $mountRoots[$mount->getStorageRootId()]; + $cacheEntry = $mount->getStorage()->getCache()->get($id); + if (!$cacheEntry) { + return null; + } + + // cache jails will hide the "true" internal path + $internalPath = ltrim($rootInternalPath . '/' . $cacheEntry->getPath(), '/'); + $pathRelativeToMount = substr($internalPath, strlen($rootInternalPath)); + $pathRelativeToMount = ltrim($pathRelativeToMount, '/'); + $absolutePath = rtrim($mount->getMountPoint() . $pathRelativeToMount, '/'); + $storage = $mount->getStorage(); + if ($storage === null) { + return null; + } + $ownerId = $storage->getOwner($pathRelativeToMount); + if ($ownerId !== false) { + $owner = Server::get(IUserManager::class)->get($ownerId); + } else { + $owner = null; + } + return $this->createNode($absolutePath, new FileInfo( + $absolutePath, + $storage, + $cacheEntry->getPath(), + $cacheEntry, + $mount, + $owner, + )); + }, $mountsContainingFile); + + $nodes = array_filter($nodes); + + $folders = array_filter($nodes, function (Node $node) use ($path) { + return PathHelper::getRelativePath($path, $node->getPath()) !== null; + }); + usort($folders, function ($a, $b) { + return $b->getPath() <=> $a->getPath(); + }); + return $folders; + } + + public function getNodeFromCacheEntryAndMount(ICacheEntry $cacheEntry, IMountPoint $mountPoint): INode { + $path = $cacheEntry->getPath(); + $fullPath = $mountPoint->getMountPoint() . $path; + // todo: LazyNode? + $info = new FileInfo($fullPath, $mountPoint->getStorage(), $path, $cacheEntry, $mountPoint); + $parentPath = dirname($fullPath); + $parent = new LazyFolder($this, function () use ($parentPath) { + $parent = $this->get($parentPath); + if ($parent instanceof \OCP\Files\Folder) { + return $parent; + } else { + throw new \Exception("parent $parentPath is not a folder"); + } + }, [ + 'path' => $parentPath, + ]); + $isDir = $info->getType() === FileInfo::TYPE_FOLDER; + $view = new View(''); + if ($isDir) { + return new Folder($this, $view, $fullPath, $info, $parent); + } else { + return new File($this, $view, $fullPath, $info, $parent); + } + } +} |