aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Files/Node
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Files/Node')
-rw-r--r--lib/private/Files/Node/File.php140
-rw-r--r--lib/private/Files/Node/Folder.php472
-rw-r--r--lib/private/Files/Node/HookConnector.php227
-rw-r--r--lib/private/Files/Node/LazyFolder.php568
-rw-r--r--lib/private/Files/Node/LazyRoot.php56
-rw-r--r--lib/private/Files/Node/LazyUserFolder.php66
-rw-r--r--lib/private/Files/Node/Node.php489
-rw-r--r--lib/private/Files/Node/NonExistingFile.php136
-rw-r--r--lib/private/Files/Node/NonExistingFolder.php172
-rw-r--r--lib/private/Files/Node/Root.php534
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);
+ }
+ }
+}