* @author Christoph Wurst * @author Joas Schilling * @author Jörn Friedrich Dreyer * @author Julius Härtl * @author Lukas Reschke * @author Morris Jobke * @author Robin Appelman * @author Roeland Jago Douma * @author Stefan Weil * @author Vincent Petry * * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License, version 3, * along with this program. If not, see * */ 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 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); } /** * @param string $path * @return Node * @throws \OCP\Files\NotPermittedException * @throws \OCP\Files\NotFoundException */ 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($path, $cachedPath)) { // getting the node by path is significantly cheaper than finding it by id $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; } } } $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 (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, '/'); return $this->createNode($absolutePath, new FileInfo( $absolutePath, $mount->getStorage(), $cacheEntry->getPath(), $cacheEntry, $mount, \OC::$server->getUserManager()->get($mount->getStorage()->getOwner($pathRelativeToMount)) )); }, $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, $path, $info, $parent); } else { return new File($this, $view, $path, $info, $parent); } } }