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); } } }