From 4d110c1dd6ae384c00c93b4e266118004b71e498 Mon Sep 17 00:00:00 2001
From: Robin Appelman <robin@icewind.nl>
Date: Thu, 8 Feb 2024 11:34:22 +0100
Subject: [PATCH 1/3] feat: add interface to get only a single node by id
 instead of all nodes for the id

this should be enough in most(?) cases and makes efficient implementation and caching easier

Signed-off-by: Robin Appelman <robin@icewind.nl>
---
 lib/private/Files/Node/Folder.php            |  6 +++++-
 lib/private/Files/Node/LazyFolder.php        |  6 +++++-
 lib/private/Files/Node/LazyRoot.php          |  5 +++++
 lib/private/Files/Node/LazyUserFolder.php    | 12 -----------
 lib/private/Files/Node/NonExistingFolder.php |  4 ++++
 lib/private/Files/Node/Root.php              |  4 ++++
 lib/public/Files/Folder.php                  | 22 +++++++++++++++++++-
 lib/public/Files/IRootFolder.php             | 18 ++++++++++++++++
 8 files changed, 62 insertions(+), 15 deletions(-)

diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php
index c7462572fed06..cb8747e0055d9 100644
--- a/lib/private/Files/Node/Folder.php
+++ b/lib/private/Files/Node/Folder.php
@@ -307,12 +307,16 @@ public function searchBySystemTag(string $tagName, string $userId, int $limit =
 
 	/**
 	 * @param int $id
-	 * @return \OC\Files\Node\Node[]
+	 * @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 current($this->getById($id));
+	}
+
 	protected function getAppDataDirectoryName(): string {
 		$instanceId = \OC::$server->getConfig()->getSystemValueString('instanceid');
 		return 'appdata_' . $instanceId;
diff --git a/lib/private/Files/Node/LazyFolder.php b/lib/private/Files/Node/LazyFolder.php
index e30cfea693ec1..4aae5cf98044d 100644
--- a/lib/private/Files/Node/LazyFolder.php
+++ b/lib/private/Files/Node/LazyFolder.php
@@ -492,7 +492,11 @@ public function searchBySystemTag(string $tagName, string $userId, int $limit =
 	 * @inheritDoc
 	 */
 	public function getById($id) {
-		return $this->__call(__FUNCTION__, func_get_args());
+		return $this->getRootFolder()->getByIdInPath((int)$id, $this->getPath());
+	}
+
+	public function getFirstNodeById(int $id): ?\OCP\Files\Node {
+		return $this->getRootFolder()->getFirstNodeByIdInPath($id, $this->getPath());
 	}
 
 	/**
diff --git a/lib/private/Files/Node/LazyRoot.php b/lib/private/Files/Node/LazyRoot.php
index 680e80cb45edc..dd1596319fa87 100644
--- a/lib/private/Files/Node/LazyRoot.php
+++ b/lib/private/Files/Node/LazyRoot.php
@@ -25,6 +25,7 @@
 use OCP\Files\Cache\ICacheEntry;
 use OCP\Files\IRootFolder;
 use OCP\Files\Mount\IMountPoint;
+use OCP\Files\Node;
 use OCP\Files\Node as INode;
 
 /**
@@ -56,6 +57,10 @@ 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);
 	}
diff --git a/lib/private/Files/Node/LazyUserFolder.php b/lib/private/Files/Node/LazyUserFolder.php
index 917ab80f3666f..3cb840ccb6d21 100644
--- a/lib/private/Files/Node/LazyUserFolder.php
+++ b/lib/private/Files/Node/LazyUserFolder.php
@@ -68,18 +68,6 @@ public function __construct(IRootFolder $rootFolder, IUser $user, IMountManager
 		]);
 	}
 
-	public function get($path) {
-		return $this->getRootFolder()->get('/' . $this->user->getUID() . '/files/' . ltrim($path, '/'));
-	}
-
-	/**
-	 * @param int $id
-	 * @return \OCP\Files\Node[]
-	 */
-	public function getById($id) {
-		return $this->getRootFolder()->getByIdInPath((int)$id, $this->getPath());
-	}
-
 	public function getMountPoint() {
 		if ($this->folder !== null) {
 			return $this->folder->getMountPoint();
diff --git a/lib/private/Files/Node/NonExistingFolder.php b/lib/private/Files/Node/NonExistingFolder.php
index 34621b18f19a3..af0ad002f241f 100644
--- a/lib/private/Files/Node/NonExistingFolder.php
+++ b/lib/private/Files/Node/NonExistingFolder.php
@@ -162,6 +162,10 @@ public function getById($id) {
 		throw new NotFoundException();
 	}
 
+	public function getFirstNodeById(int $id): ?\OCP\Files\Node {
+		throw new NotFoundException();
+	}
+
 	public function getFreeSpace() {
 		throw new NotFoundException();
 	}
diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php
index 9bfb610199b59..bc76640966e43 100644
--- a/lib/private/Files/Node/Root.php
+++ b/lib/private/Files/Node/Root.php
@@ -405,6 +405,10 @@ public function getUserMountCache() {
 		return $this->userMountCache;
 	}
 
+	public function getFirstNodeByIdInPath(int $id, string $path): ?INode {
+		return current($this->getByIdInPath($id, $path));
+	}
+
 	/**
 	 * @param int $id
 	 * @return Node[]
diff --git a/lib/public/Files/Folder.php b/lib/public/Files/Folder.php
index eb81a2098ec33..945df48a13e0a 100644
--- a/lib/public/Files/Folder.php
+++ b/lib/public/Files/Folder.php
@@ -152,17 +152,37 @@ public function searchByTag($tag, $userId);
 	public function searchBySystemTag(string $tagName, string $userId, int $limit = 0, int $offset = 0);
 
 	/**
-	 * get a file or folder inside the folder by it's internal id
+	 * get a file or folder inside the folder by its internal id
 	 *
 	 * This method could return multiple entries. For example once the file/folder
 	 * is shared or mounted (files_external) to the user multiple times.
 	 *
+	 * Note that the different entries can have different permissions.
+	 *
 	 * @param int $id
 	 * @return \OCP\Files\Node[]
 	 * @since 6.0.0
 	 */
 	public function getById($id);
 
+	/**
+	 * get a file or folder inside the folder by its internal id
+	 *
+	 * Unlike getById, this method only returns a single node even if the user has
+	 * access to the file with the requested id multiple times.
+	 *
+	 * This method provides no guarantee about which of the nodes in returned and the
+	 * returned node might, for example, have less permissions than other nodes for the same file
+	 *
+	 * Apps that require accurate information about the users access to the file should use getById
+	 * instead of pick the correct node out of the result.
+	 *
+	 * @param int $id
+	 * @return Node|null
+	 * @since 29.0.0
+	 */
+	public function getFirstNodeById(int $id): ?Node;
+
 	/**
 	 * Get the amount of free space inside the folder
 	 *
diff --git a/lib/public/Files/IRootFolder.php b/lib/public/Files/IRootFolder.php
index 44f0ba5f2e110..c1c0e6e8c72c0 100644
--- a/lib/public/Files/IRootFolder.php
+++ b/lib/public/Files/IRootFolder.php
@@ -59,6 +59,24 @@ public function getUserFolder($userId);
 	 */
 	public function getByIdInPath(int $id, string $path);
 
+	/**
+	 * get a file or folder inside the folder by its internal id
+	 *
+	 * Unlike getByIdInPath, this method only returns a single node even if the user has
+	 * access to the file with the requested id multiple times.
+	 *
+	 * This method provides no guarantee about which of the nodes in returned and the
+	 * returned node might, for example, have less permissions than other nodes for the same file
+	 *
+	 * Apps that require accurate information about the users access to the file should use getByIdInPath
+	 * instead of pick the correct node out of the result.
+	 *
+	 * @param int $id
+	 * @return Node|null
+	 * @since 29.0.0
+	 */
+	public function getFirstNodeByIdInPath(int $id, string $path): ?Node;
+
 	/**
 	 * @return IMountPoint[]
 	 *

From e7a7b4a40184dc3da2c83e858c820625f660e48e Mon Sep 17 00:00:00 2001
From: Robin Appelman <robin@icewind.nl>
Date: Fri, 9 Feb 2024 09:54:52 +0100
Subject: [PATCH 2/3] perf: switch places that always use the first getById
 result to getFirstNodeById

Signed-off-by: Robin Appelman <robin@icewind.nl>
---
 apps/dav/lib/BulkUpload/BulkUploadPlugin.php  |   2 +-
 .../lib/Connector/Sabre/FilesReportPlugin.php |   6 +-
 apps/dav/lib/Controller/DirectController.php  |   5 +-
 apps/dav/lib/Direct/DirectFile.php            |   9 +-
 apps/dav/lib/RootCollection.php               |   3 +-
 .../SystemTagsRelationsCollection.php         |  13 +-
 .../Connector/Sabre/FilesReportPluginTest.php |  12 +-
 .../unit/Controller/DirectControllerTest.php  |   8 +-
 apps/dav/tests/unit/Direct/DirectFileTest.php |   4 +-
 .../SystemTagsObjectTypeCollectionTest.php    |  32 ++---
 .../lib/FederatedShareProvider.php            |   8 +-
 apps/files/lib/Activity/Helper.php            |   5 +-
 apps/files/lib/Activity/Provider.php          |   6 +-
 .../lib/BackgroundJob/TransferOwnership.php   |   6 +-
 .../Resources/ResourceProvider.php            |  12 +-
 apps/files/lib/Controller/ViewController.php  |  17 +--
 .../lib/Listener/SyncLivePhotosListener.php   |   6 +-
 .../tests/Controller/ViewControllerTest.php   |  10 +-
 .../lib/Model/RichReminder.php                |   5 +-
 .../lib/Notification/Notifier.php             |   5 +-
 .../lib/Service/ReminderService.php           |   4 +-
 .../Collaboration/ShareRecipientSorter.php    |   6 +-
 .../Controller/DeletedShareAPIController.php  |   6 +-
 .../lib/Controller/ShareAPIController.php     |  30 ++---
 .../lib/Controller/ShareController.php        |   3 +-
 apps/files_sharing/lib/OrphanHelper.php       |   4 +-
 apps/files_sharing/lib/SharedStorage.php      |   4 +-
 .../ShareRecipientSorterTest.php              |  10 +-
 .../Controller/ShareAPIControllerTest.php     | 124 ++++++++++--------
 .../tests/Controller/ShareControllerTest.php  |  47 ++++---
 .../files_sharing/tests/SharedStorageTest.php |   1 +
 .../lib/Trash/LegacyTrashBackend.php          |   7 +-
 apps/files_versions/lib/Sabre/VersionRoot.php |   6 +-
 apps/files_versions/lib/Storage.php           |   6 +-
 .../lib/Versions/LegacyVersionsBackend.php    |   3 +-
 core/Command/FilesMetadata/Get.php            |   6 +-
 core/Command/Info/FileUtils.php               |   6 +-
 core/Command/Preview/Generate.php             |   6 +-
 core/Controller/PreviewController.php         |   6 +-
 .../Reference/File/FileReferenceProvider.php  |   8 +-
 lib/private/DirectEditing/Manager.php         |   6 +-
 lib/private/Files/Config/CachedMountInfo.php  |   7 +-
 lib/private/Files/Node/Folder.php             |   2 +-
 .../Job/UpdateSingleMetadata.php              |   7 +-
 lib/private/Share20/Manager.php               |   6 +-
 lib/private/Share20/Share.php                 |  14 +-
 lib/private/SpeechToText/TranscriptionJob.php |   2 +-
 lib/public/Share/IShare.php                   |   2 +-
 .../lib/Share20/DefaultShareProviderTest.php  | 104 +++++++--------
 tests/lib/Share20/ManagerTest.php             |  12 +-
 50 files changed, 316 insertions(+), 313 deletions(-)

diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
index 4d838d255eb08..9890a615f9364 100644
--- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
+++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
@@ -91,7 +91,7 @@ public function httpPost(RequestInterface $request, ResponseInterface $response)
 
 				$node = $this->userFolder->newFile($headers['x-file-path'], $content);
 				$node->touch($mtime);
-				$node = $this->userFolder->getById($node->getId())[0];
+				$node = $this->userFolder->getFirstNodeById($node->getId());
 
 				$writtenFiles[$headers['x-file-path']] = [
 					"error" => false,
diff --git a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
index e43fb740cac7b..6a8cb3f0f59a1 100644
--- a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
@@ -424,14 +424,14 @@ public function findNodesByFileIds(Node $rootNode, array $fileIds): array {
 		}
 		$folder = $this->userFolder;
 		if (trim($rootNode->getPath(), '/') !== '') {
+			/** @var Folder $folder */
 			$folder = $folder->get($rootNode->getPath());
 		}
 
 		$results = [];
 		foreach ($fileIds as $fileId) {
-			$entry = $folder->getById($fileId);
+			$entry = $folder->getFirstNodeById($fileId);
 			if ($entry) {
-				$entry = current($entry);
 				$results[] = $this->wrapNode($entry);
 			}
 		}
@@ -439,7 +439,7 @@ public function findNodesByFileIds(Node $rootNode, array $fileIds): array {
 		return $results;
 	}
 
-	protected function wrapNode(\OCP\Files\File|\OCP\Files\Folder $node): File|Directory {
+	protected function wrapNode(\OCP\Files\Node $node): File|Directory {
 		if ($node instanceof \OCP\Files\File) {
 			return new File($this->fileView, $node);
 		} else {
diff --git a/apps/dav/lib/Controller/DirectController.php b/apps/dav/lib/Controller/DirectController.php
index 0f618e46b2275..2d4522c8d378d 100644
--- a/apps/dav/lib/Controller/DirectController.php
+++ b/apps/dav/lib/Controller/DirectController.php
@@ -104,9 +104,9 @@ public function __construct(string $appName,
 	public function getUrl(int $fileId, int $expirationTime = 60 * 60 * 8): DataResponse {
 		$userFolder = $this->rootFolder->getUserFolder($this->userId);
 
-		$files = $userFolder->getById($fileId);
+		$file = $userFolder->getFirstNodeById($fileId);
 
-		if ($files === []) {
+		if (!$file) {
 			throw new OCSNotFoundException();
 		}
 
@@ -114,7 +114,6 @@ public function getUrl(int $fileId, int $expirationTime = 60 * 60 * 8): DataResp
 			throw new OCSBadRequestException('Expiration time should be greater than 0 and less than or equal to ' . (60 * 60 * 24));
 		}
 
-		$file = array_shift($files);
 		if (!($file instanceof File)) {
 			throw new OCSBadRequestException('Direct download only works for files');
 		}
diff --git a/apps/dav/lib/Direct/DirectFile.php b/apps/dav/lib/Direct/DirectFile.php
index 45c2114747eb7..b9d1757cfc288 100644
--- a/apps/dav/lib/Direct/DirectFile.php
+++ b/apps/dav/lib/Direct/DirectFile.php
@@ -108,13 +108,16 @@ public function getLastModified() {
 	private function getFile() {
 		if ($this->file === null) {
 			$userFolder = $this->rootFolder->getUserFolder($this->direct->getUserId());
-			$files = $userFolder->getById($this->direct->getFileId());
+			$file = $userFolder->getFirstNodeById($this->direct->getFileId());
 
-			if ($files === []) {
+			if (!$file) {
 				throw new NotFound();
 			}
+			if (!$file instanceof File) {
+				throw new Forbidden("direct download not allowed on directories");
+			}
 
-			$this->file = array_shift($files);
+			$this->file = $file;
 		}
 
 		return $this->file;
diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php
index 0e9af0a427626..d1f3dbc91bded 100644
--- a/apps/dav/lib/RootCollection.php
+++ b/apps/dav/lib/RootCollection.php
@@ -134,7 +134,8 @@ public function __construct() {
 			\OC::$server->getSystemTagObjectMapper(),
 			\OC::$server->getUserSession(),
 			$groupManager,
-			$dispatcher
+			$dispatcher,
+			$rootFolder,
 		);
 		$systemTagInUseCollection = \OCP\Server::get(SystemTag\SystemTagsInUseCollection::class);
 		$commentsCollection = new Comments\RootCollection(
diff --git a/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php b/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php
index 6c44713cb0566..6911d40e208a3 100644
--- a/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php
+++ b/apps/dav/lib/SystemTag/SystemTagsRelationsCollection.php
@@ -27,6 +27,7 @@
 namespace OCA\DAV\SystemTag;
 
 use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\IRootFolder;
 use OCP\IGroupManager;
 use OCP\IUserSession;
 use OCP\SystemTag\ISystemTagManager;
@@ -42,6 +43,7 @@ public function __construct(
 		IUserSession $userSession,
 		IGroupManager $groupManager,
 		IEventDispatcher $dispatcher,
+		IRootFolder $rootFolder,
 	) {
 		$children = [
 			new SystemTagsObjectTypeCollection(
@@ -50,9 +52,14 @@ public function __construct(
 				$tagMapper,
 				$userSession,
 				$groupManager,
-				function ($name) {
-					$nodes = \OC::$server->getUserFolder()->getById((int)$name);
-					return !empty($nodes);
+				function (string $name) use ($rootFolder, $userSession): bool {
+					$user = $userSession->getUser();
+					if ($user) {
+						$node = $rootFolder->getUserFolder($user->getUID())->getFirstNodeById((int)$name);
+						return $node !== null;
+					} else {
+						return false;
+					}
 				}
 			),
 		];
diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
index c636d16358dad..d60d1123de955 100644
--- a/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php
@@ -320,14 +320,14 @@ public function testFindNodesByFileIdsRoot(): void {
 			->willReturn('/');
 
 		$this->userFolder->expects($this->exactly(2))
-			->method('getById')
+			->method('getFirstNodeById')
 			->withConsecutive(
 				['111'],
 				['222'],
 			)
 			->willReturnOnConsecutiveCalls(
-				[$filesNode1],
-				[$filesNode2],
+				$filesNode1,
+				$filesNode2,
 			);
 
 		/** @var \OCA\DAV\Connector\Sabre\Directory|MockObject $reportTargetNode */
@@ -373,14 +373,14 @@ public function testFindNodesByFileIdsSubDir(): void {
 			->willReturn($subNode);
 
 		$subNode->expects($this->exactly(2))
-			->method('getById')
+			->method('getFirstNodeById')
 			->withConsecutive(
 				['111'],
 				['222'],
 			)
 			->willReturnOnConsecutiveCalls(
-				[$filesNode1],
-				[$filesNode2],
+				$filesNode1,
+				$filesNode2,
 			);
 
 		/** @var \OCA\DAV\Connector\Sabre\Directory|MockObject $reportTargetNode */
diff --git a/apps/dav/tests/unit/Controller/DirectControllerTest.php b/apps/dav/tests/unit/Controller/DirectControllerTest.php
index fe6d4ea8f24b1..181b02bda2c98 100644
--- a/apps/dav/tests/unit/Controller/DirectControllerTest.php
+++ b/apps/dav/tests/unit/Controller/DirectControllerTest.php
@@ -110,9 +110,9 @@ public function testGetUrlForFolder(): void {
 
 		$folder = $this->createMock(Folder::class);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(101)
-			->willReturn([$folder]);
+			->willReturn($folder);
 
 		$this->expectException(OCSBadRequestException::class);
 		$this->controller->getUrl(101);
@@ -129,9 +129,9 @@ public function testGetUrlValid(): void {
 		$this->timeFactory->method('getTime')
 			->willReturn(42);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(101)
-			->willReturn([$file]);
+			->willReturn($file);
 
 		$userFolder->method('getRelativePath')
 			->willReturn('/path');
diff --git a/apps/dav/tests/unit/Direct/DirectFileTest.php b/apps/dav/tests/unit/Direct/DirectFileTest.php
index fdf7fa54923ce..bc8b382253244 100644
--- a/apps/dav/tests/unit/Direct/DirectFileTest.php
+++ b/apps/dav/tests/unit/Direct/DirectFileTest.php
@@ -73,9 +73,9 @@ protected function setUp(): void {
 			->willReturn($this->userFolder);
 
 		$this->file = $this->createMock(File::class);
-		$this->userFolder->method('getById')
+		$this->userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$this->file]);
+			->willReturn($this->file);
 
 		$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
 
diff --git a/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php b/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php
index 4dd81615c018c..143d598fd2dcf 100644
--- a/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php
+++ b/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php
@@ -84,8 +84,8 @@ protected function setUp(): void {
 		$userFolder = $this->userFolder;
 
 		$closure = function ($name) use ($userFolder) {
-			$nodes = $userFolder->getById(intval($name));
-			return !empty($nodes);
+			$node = $userFolder->getFirstNodeById(intval($name));
+			return $node !== null;
 		};
 
 		$this->node = new \OCA\DAV\SystemTag\SystemTagsObjectTypeCollection(
@@ -98,14 +98,14 @@ protected function setUp(): void {
 		);
 	}
 
-	
+
 	public function testForbiddenCreateFile(): void {
 		$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
 
 		$this->node->createFile('555');
 	}
 
-	
+
 	public function testForbiddenCreateDirectory(): void {
 		$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
 
@@ -114,27 +114,27 @@ public function testForbiddenCreateDirectory(): void {
 
 	public function testGetChild(): void {
 		$this->userFolder->expects($this->once())
-			->method('getById')
+			->method('getFirstNodeById')
 			->with('555')
-			->willReturn([true]);
+			->willReturn($this->createMock(\OCP\Files\Node::class));
 		$childNode = $this->node->getChild('555');
 
 		$this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection', $childNode);
 		$this->assertEquals('555', $childNode->getName());
 	}
 
-	
+
 	public function testGetChildWithoutAccess(): void {
 		$this->expectException(\Sabre\DAV\Exception\NotFound::class);
 
 		$this->userFolder->expects($this->once())
-			->method('getById')
+			->method('getFirstNodeById')
 			->with('555')
-			->willReturn([]);
+			->willReturn(null);
 		$this->node->getChild('555');
 	}
 
-	
+
 	public function testGetChildren(): void {
 		$this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class);
 
@@ -143,28 +143,28 @@ public function testGetChildren(): void {
 
 	public function testChildExists(): void {
 		$this->userFolder->expects($this->once())
-			->method('getById')
+			->method('getFirstNodeById')
 			->with('123')
-			->willReturn([true]);
+			->willReturn($this->createMock(\OCP\Files\Node::class));
 		$this->assertTrue($this->node->childExists('123'));
 	}
 
 	public function testChildExistsWithoutAccess(): void {
 		$this->userFolder->expects($this->once())
-			->method('getById')
+			->method('getFirstNodeById')
 			->with('555')
-			->willReturn([]);
+			->willReturn(null);
 		$this->assertFalse($this->node->childExists('555'));
 	}
 
-	
+
 	public function testDelete(): void {
 		$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
 
 		$this->node->delete();
 	}
 
-	
+
 	public function testSetName(): void {
 		$this->expectException(\Sabre\DAV\Exception\Forbidden::class);
 
diff --git a/apps/federatedfilesharing/lib/FederatedShareProvider.php b/apps/federatedfilesharing/lib/FederatedShareProvider.php
index 69813289cae38..45efd0c7c8a0b 100644
--- a/apps/federatedfilesharing/lib/FederatedShareProvider.php
+++ b/apps/federatedfilesharing/lib/FederatedShareProvider.php
@@ -880,7 +880,7 @@ private function createShareObject($data) {
 	 *
 	 * @param string $userId
 	 * @param int $id
-	 * @return \OCP\Files\File|\OCP\Files\Folder
+	 * @return \OCP\Files\Node
 	 * @throws InvalidShare
 	 */
 	private function getNode($userId, $id) {
@@ -890,13 +890,13 @@ private function getNode($userId, $id) {
 			throw new InvalidShare();
 		}
 
-		$nodes = $userFolder->getById($id);
+		$node = $userFolder->getFirstNodeById($id);
 
-		if (empty($nodes)) {
+		if (!$node) {
 			throw new InvalidShare();
 		}
 
-		return $nodes[0];
+		return $node;
 	}
 
 	/**
diff --git a/apps/files/lib/Activity/Helper.php b/apps/files/lib/Activity/Helper.php
index 7bbaf44ab4cc8..9ad790f51542e 100644
--- a/apps/files/lib/Activity/Helper.php
+++ b/apps/files/lib/Activity/Helper.php
@@ -61,9 +61,8 @@ public function getFavoriteNodes(string $user, bool $foldersOnly = false): array
 		$userFolder = $this->rootFolder->getUserFolder($user);
 		$favoriteNodes = [];
 		foreach ($favorites as $favorite) {
-			$nodes = $userFolder->getById($favorite);
-			if (!empty($nodes)) {
-				$node = array_shift($nodes);
+			$node = $userFolder->getFirstNodeById($favorite);
+			if ($node) {
 				if (!$foldersOnly || $node instanceof Folder) {
 					$favoriteNodes[] = $node;
 				}
diff --git a/apps/files/lib/Activity/Provider.php b/apps/files/lib/Activity/Provider.php
index fb81e200688fa..50535cab5c6d2 100644
--- a/apps/files/lib/Activity/Provider.php
+++ b/apps/files/lib/Activity/Provider.php
@@ -433,8 +433,8 @@ protected function getEndToEndEncryptionContainer($fileId, $path) {
 		}
 
 		$userFolder = $this->rootFolder->getUserFolder($this->activityManager->getCurrentUserId());
-		$files = $userFolder->getById($fileId);
-		if (empty($files)) {
+		$file = $userFolder->getFirstNodeById($fileId);
+		if (!$file) {
 			try {
 				// Deleted, try with parent
 				$file = $this->findExistingParent($userFolder, dirname($path));
@@ -450,8 +450,6 @@ protected function getEndToEndEncryptionContainer($fileId, $path) {
 			return $file;
 		}
 
-		$file = array_shift($files);
-
 		if ($file instanceof Folder && $file->isEncrypted()) {
 			// If the folder is encrypted, it is the Container,
 			// but can be the name is just fine.
diff --git a/apps/files/lib/BackgroundJob/TransferOwnership.php b/apps/files/lib/BackgroundJob/TransferOwnership.php
index 1f182b5e999ab..5cf35396d43d2 100644
--- a/apps/files/lib/BackgroundJob/TransferOwnership.php
+++ b/apps/files/lib/BackgroundJob/TransferOwnership.php
@@ -61,14 +61,14 @@ protected function run($argument) {
 		$fileId = $transfer->getFileId();
 
 		$userFolder = $this->rootFolder->getUserFolder($sourceUser);
-		$nodes = $userFolder->getById($fileId);
+		$node = $userFolder->getFirstNodeById($fileId);
 
-		if (empty($nodes)) {
+		if (!$node) {
 			$this->logger->alert('Could not transfer ownership: Node not found');
 			$this->failedNotication($transfer);
 			return;
 		}
-		$path = $userFolder->getRelativePath($nodes[0]->getPath());
+		$path = $userFolder->getRelativePath($node->getPath());
 
 		$sourceUserObject = $this->userManager->get($sourceUser);
 		$destinationUserObject = $this->userManager->get($destinationUser);
diff --git a/apps/files/lib/Collaboration/Resources/ResourceProvider.php b/apps/files/lib/Collaboration/Resources/ResourceProvider.php
index 4c5afc76b2bbc..a15e15580572c 100644
--- a/apps/files/lib/Collaboration/Resources/ResourceProvider.php
+++ b/apps/files/lib/Collaboration/Resources/ResourceProvider.php
@@ -60,9 +60,9 @@ private function getNode(IResource $resource): ?Node {
 		if (isset($this->nodes[(int) $resource->getId()])) {
 			return $this->nodes[(int) $resource->getId()];
 		}
-		$nodes = $this->rootFolder->getById((int) $resource->getId());
-		if (!empty($nodes)) {
-			$this->nodes[(int) $resource->getId()] = array_shift($nodes);
+		$node = $this->rootFolder->getFirstNodeById((int) $resource->getId());
+		if ($node) {
+			$this->nodes[(int) $resource->getId()] = $node;
 			return $this->nodes[(int) $resource->getId()];
 		}
 		return null;
@@ -113,10 +113,10 @@ public function canAccessResource(IResource $resource, IUser $user = null): bool
 		}
 
 		$userFolder = $this->rootFolder->getUserFolder($user->getUID());
-		$nodes = $userFolder->getById((int) $resource->getId());
+		$node = $userFolder->getById((int) $resource->getId());
 
-		if (!empty($nodes)) {
-			$this->nodes[(int) $resource->getId()] = array_shift($nodes);
+		if ($node) {
+			$this->nodes[(int) $resource->getId()] = $node;
 			return true;
 		}
 
diff --git a/apps/files/lib/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php
index e7398cc21c2ba..12de4e1f6a200 100644
--- a/apps/files/lib/Controller/ViewController.php
+++ b/apps/files/lib/Controller/ViewController.php
@@ -302,8 +302,7 @@ private function provideInitialState(string $dir, ?string $fileid): void {
 
 		$uid = $user->getUID();
 		$userFolder = $this->rootFolder->getUserFolder($uid);
-		$nodes = $userFolder->getById((int) $fileid);
-		$node = array_shift($nodes);
+		$node = $userFolder->getFirstNodeById((int) $fileid);
 
 		if ($node === null) {
 			return;
@@ -343,17 +342,16 @@ private function provideInitialState(string $dir, ?string $fileid): void {
 	private function redirectToFileIfInTrashbin($fileId): RedirectResponse {
 		$uid = $this->userSession->getUser()->getUID();
 		$baseFolder = $this->rootFolder->getUserFolder($uid);
-		$nodes = $baseFolder->getById($fileId);
+		$node = $baseFolder->getFirstNodeById($fileId);
 		$params = [];
 
-		if (empty($nodes) && $this->appManager->isEnabledForUser('files_trashbin')) {
+		if (!$node && $this->appManager->isEnabledForUser('files_trashbin')) {
 			/** @var Folder */
 			$baseFolder = $this->rootFolder->get($uid . '/files_trashbin/files/');
-			$nodes = $baseFolder->getById($fileId);
+			$node = $baseFolder->getFirstNodeById($fileId);
 			$params['view'] = 'trashbin';
 
-			if (!empty($nodes)) {
-				$node = current($nodes);
+			if ($node) {
 				$params['fileid'] = $fileId;
 				if ($node instanceof Folder) {
 					// set the full path to enter the folder
@@ -378,7 +376,7 @@ private function redirectToFileIfInTrashbin($fileId): RedirectResponse {
 	private function redirectToFile(int $fileId) {
 		$uid = $this->userSession->getUser()->getUID();
 		$baseFolder = $this->rootFolder->getUserFolder($uid);
-		$nodes = $baseFolder->getById($fileId);
+		$node = $baseFolder->getFirstNodeById($fileId);
 		$params = ['view' => 'files'];
 
 		try {
@@ -386,8 +384,7 @@ private function redirectToFile(int $fileId) {
 		} catch (NotFoundException $e) {
 		}
 
-		if (!empty($nodes)) {
-			$node = current($nodes);
+		if ($node) {
 			$params['fileid'] = $fileId;
 			if ($node instanceof Folder) {
 				// set the full path to enter the folder
diff --git a/apps/files/lib/Listener/SyncLivePhotosListener.php b/apps/files/lib/Listener/SyncLivePhotosListener.php
index b188ad24073eb..ea63c08f94d16 100644
--- a/apps/files/lib/Listener/SyncLivePhotosListener.php
+++ b/apps/files/lib/Listener/SyncLivePhotosListener.php
@@ -233,9 +233,9 @@ private function getLivePhotoPeer(int $nodeId): ?Node {
 		$peerFileId = (int)$metadata->getString('files-live-photo');
 
 		// Check the user's folder.
-		$nodes = $this->userFolder->getById($peerFileId);
-		if (count($nodes) !== 0) {
-			return $nodes[0];
+		$node = $this->userFolder->getFirstNodeById($peerFileId);
+		if ($node) {
+			return $node;
 		}
 
 		// Check the user's trashbin.
diff --git a/apps/files/tests/Controller/ViewControllerTest.php b/apps/files/tests/Controller/ViewControllerTest.php
index 0b71011f60cd9..2714a6b25c0fa 100644
--- a/apps/files/tests/Controller/ViewControllerTest.php
+++ b/apps/files/tests/Controller/ViewControllerTest.php
@@ -167,7 +167,7 @@ public function testIndexWithRegularBrowser() {
 				[$this->user->getUID(), 'files', 'crop_image_previews', true, true],
 				[$this->user->getUID(), 'files', 'show_grid', true],
 			]);
-		
+
 		$baseFolderFiles = $this->getMockBuilder(Folder::class)->getMock();
 
 		$this->rootFolder->expects($this->any())
@@ -228,9 +228,9 @@ public function testShowFileRouteWithTrashedFile() {
 			->willReturn($baseFolderTrash);
 
 		$baseFolderFiles->expects($this->any())
-			->method('getById')
+			->method('getFirstNodeById')
 			->with(123)
-			->willReturn([]);
+			->willReturn(null);
 
 		$node = $this->getMockBuilder(File::class)->getMock();
 		$node->expects($this->once())
@@ -238,9 +238,9 @@ public function testShowFileRouteWithTrashedFile() {
 			->willReturn($parentNode);
 
 		$baseFolderTrash->expects($this->once())
-			->method('getById')
+			->method('getFirstNodeById')
 			->with(123)
-			->willReturn([$node]);
+			->willReturn($node);
 		$baseFolderTrash->expects($this->once())
 			->method('getRelativePath')
 			->with('testuser1/files_trashbin/files/test.d1462861890/sub')
diff --git a/apps/files_reminders/lib/Model/RichReminder.php b/apps/files_reminders/lib/Model/RichReminder.php
index 10dc89799fe99..f621a8f16d57c 100644
--- a/apps/files_reminders/lib/Model/RichReminder.php
+++ b/apps/files_reminders/lib/Model/RichReminder.php
@@ -45,11 +45,10 @@ public function __construct(
 	 * @throws NodeNotFoundException
 	 */
 	public function getNode(): Node {
-		$nodes = $this->root->getUserFolder($this->getUserId())->getById($this->getFileId());
-		if (empty($nodes)) {
+		$node = $this->root->getUserFolder($this->getUserId())->getFirstNodeById($this->getFileId());
+		if (!$node) {
 			throw new NodeNotFoundException();
 		}
-		$node = reset($nodes);
 		return $node;
 	}
 
diff --git a/apps/files_reminders/lib/Notification/Notifier.php b/apps/files_reminders/lib/Notification/Notifier.php
index f7ffa0b443754..a280c4d9336a2 100644
--- a/apps/files_reminders/lib/Notification/Notifier.php
+++ b/apps/files_reminders/lib/Notification/Notifier.php
@@ -69,11 +69,10 @@ public function prepare(INotification $notification, string $languageCode): INot
 				$params = $notification->getSubjectParameters();
 				$fileId = $params['fileId'];
 
-				$nodes = $this->root->getUserFolder($notification->getUser())->getById($fileId);
-				if (empty($nodes)) {
+				$node = $this->root->getUserFolder($notification->getUser())->getFirstNodeById($fileId);
+				if (!$node) {
 					throw new InvalidArgumentException();
 				}
-				$node = reset($nodes);
 
 				$path = rtrim($node->getPath(), '/');
 				if (strpos($path, '/' . $notification->getUser() . '/files/') === 0) {
diff --git a/apps/files_reminders/lib/Service/ReminderService.php b/apps/files_reminders/lib/Service/ReminderService.php
index fabca65c45a19..9d5daf0cc5e00 100644
--- a/apps/files_reminders/lib/Service/ReminderService.php
+++ b/apps/files_reminders/lib/Service/ReminderService.php
@@ -98,8 +98,8 @@ public function createOrUpdate(IUser $user, int $fileId, DateTime $dueDate): boo
 			$this->reminderMapper->update($reminder);
 			return false;
 		} catch (DoesNotExistException $e) {
-			$nodes = $this->root->getUserFolder($user->getUID())->getById($fileId);
-			if (empty($nodes)) {
+			$node = $this->root->getUserFolder($user->getUID())->getFirstNodeById($fileId);
+			if (!$node) {
 				throw new NodeNotFoundException();
 			}
 			// Create new reminder if no reminder is found
diff --git a/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php b/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php
index 4ed30f44020e0..ee2d0e9885ec9 100644
--- a/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php
+++ b/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php
@@ -56,11 +56,11 @@ public function sort(array &$sortArray, array $context) {
 		}
 		$userFolder = $this->rootFolder->getUserFolder($user->getUID());
 		/** @var Node[] $nodes */
-		$nodes = $userFolder->getById((int)$context['itemId']);
-		if (count($nodes) === 0) {
+		$node = $userFolder->getFirstNodeById((int)$context['itemId']);
+		if (!$node) {
 			return;
 		}
-		$al = $this->shareManager->getAccessList($nodes[0]);
+		$al = $this->shareManager->getAccessList($node);
 
 		foreach ($sortArray as $type => &$byType) {
 			if (!isset($al[$type]) || !is_array($al[$type])) {
diff --git a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php
index 6b54a3489c6e3..c405c382329a5 100644
--- a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php
+++ b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php
@@ -117,15 +117,13 @@ private function formatShare(IShare $share): array {
 			'path' => $share->getTarget(),
 		];
 		$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
-		$nodes = $userFolder->getById($share->getNodeId());
-		if (empty($nodes)) {
+		$node = $userFolder->getFirstNodeById($share->getNodeId());
+		if (!$node) {
 			// fallback to guessing the path
 			$node = $userFolder->get($share->getTarget());
 			if ($node === null || $share->getTarget() === '') {
 				throw new NotFoundException();
 			}
-		} else {
-			$node = $nodes[0];
 		}
 
 		$result['path'] = $userFolder->getRelativePath($node->getPath());
diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php
index ea124382d9a9e..c9a1486db2fe2 100644
--- a/apps/files_sharing/lib/Controller/ShareAPIController.php
+++ b/apps/files_sharing/lib/Controller/ShareAPIController.php
@@ -203,15 +203,13 @@ protected function formatShare(IShare $share, Node $recipientNode = null): array
 		if ($recipientNode) {
 			$node = $recipientNode;
 		} else {
-			$nodes = $userFolder->getById($share->getNodeId());
-			if (empty($nodes)) {
+			$node = $userFolder->getFirstNodeById($share->getNodeId());
+			if (!$node) {
 				// fallback to guessing the path
 				$node = $userFolder->get($share->getTarget());
 				if ($node === null || $share->getTarget() === '') {
 					throw new NotFoundException();
 				}
-			} else {
-				$node = reset($nodes);
 			}
 		}
 
@@ -1142,8 +1140,7 @@ public function getInheritedShares(string $path): DataResponse {
 			$owner = $node->getOwner()
 						  ->getUID();
 			$userFolder = $this->rootFolder->getUserFolder($owner);
-			$nodes = $userFolder->getById($node->getId());
-			$node = array_shift($nodes);
+			$node = $userFolder->getFirstNodeById($node->getId());
 		}
 		$basePath = $userFolder->getPath();
 
@@ -1164,9 +1161,9 @@ public function getInheritedShares(string $path): DataResponse {
 		foreach ($nodes as $node) {
 			$getShares = $this->getFormattedShares($owner, $node, false, true);
 
-			$currentUserNodes = $currentUserFolder->getById($node->getId());
-			if (!empty($currentUserNodes)) {
-				$parent = array_pop($currentUserNodes);
+			$currentUserNode = $currentUserFolder->getFirstNodeById($node->getId());
+			if ($currentUserNode) {
+				$parent = $currentUserNode;
 			}
 
 			$subPath = $currentUserFolder->getRelativePath($parent->getPath());
@@ -1423,15 +1420,13 @@ public function pendingShares(): DataResponse {
 
 		$result = array_filter(array_map(function (IShare $share) {
 			$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
-			$nodes = $userFolder->getById($share->getNodeId());
-			if (empty($nodes)) {
+			$node = $userFolder->getFirstNodeById($share->getNodeId());
+			if (!$node) {
 				// fallback to guessing the path
 				$node = $userFolder->get($share->getTarget());
 				if ($node === null || $share->getTarget() === '') {
 					return null;
 				}
-			} else {
-				$node = $nodes[0];
 			}
 
 			try {
@@ -1516,8 +1511,8 @@ protected function canAccessShare(\OCP\Share\IShare $share, bool $checkGroups =
 		// Have reshare rights on the shared file/folder ?
 		// Does the currentUser have access to the shared file?
 		$userFolder = $this->rootFolder->getUserFolder($this->currentUser);
-		$files = $userFolder->getById($share->getNodeId());
-		if (!empty($files) && $this->shareProviderResharingRights($this->currentUser, $share, $files[0])) {
+		$file = $userFolder->getFirstNodeById($share->getNodeId());
+		if ($file && $this->shareProviderResharingRights($this->currentUser, $share, $file)) {
 			return true;
 		}
 
@@ -2091,11 +2086,10 @@ private function checkInheritedAttributes(IShare $share): void {
 			return; // Probably in a test
 		}
 		$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
-		$nodes = $userFolder->getById($share->getNodeId());
-		if (empty($nodes)) {
+		$node = $userFolder->getFirstNodeById($share->getNodeId());
+		if (!$node) {
 			return;
 		}
-		$node = $nodes[0];
 		if ($node->getStorage()->instanceOfStorage(SharedStorage::class)) {
 			$storage = $node->getStorage();
 			if ($storage instanceof Wrapper) {
diff --git a/apps/files_sharing/lib/Controller/ShareController.php b/apps/files_sharing/lib/Controller/ShareController.php
index 0691137631b5c..d6780d37f2fce 100644
--- a/apps/files_sharing/lib/Controller/ShareController.php
+++ b/apps/files_sharing/lib/Controller/ShareController.php
@@ -527,8 +527,7 @@ protected function singleFileDownloaded(Share\IShare $share, \OCP\Files\Node $no
 		$fileId = $node->getId();
 
 		$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
-		$userNodeList = $userFolder->getById($fileId);
-		$userNode = $userNodeList[0];
+		$userNode = $userFolder->getFirstNodeById($fileId);
 		$ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
 		$userPath = $userFolder->getRelativePath($userNode->getPath());
 		$ownerPath = $ownerFolder->getRelativePath($node->getPath());
diff --git a/apps/files_sharing/lib/OrphanHelper.php b/apps/files_sharing/lib/OrphanHelper.php
index e4fd062db3b20..7dfaadc773b19 100644
--- a/apps/files_sharing/lib/OrphanHelper.php
+++ b/apps/files_sharing/lib/OrphanHelper.php
@@ -46,8 +46,8 @@ public function isShareValid(string $owner, int $fileId): bool {
 		} catch (NoUserException $e) {
 			return false;
 		}
-		$nodes = $userFolder->getById($fileId);
-		return count($nodes) > 0;
+		$node = $userFolder->getFirstNodeById($fileId);
+		return $node !== null;
 	}
 
 	/**
diff --git a/apps/files_sharing/lib/SharedStorage.php b/apps/files_sharing/lib/SharedStorage.php
index 6b605a6eddb3a..35e2c0a7e36f9 100644
--- a/apps/files_sharing/lib/SharedStorage.php
+++ b/apps/files_sharing/lib/SharedStorage.php
@@ -142,9 +142,7 @@ private function init() {
 			$rootFolder = \OC::$server->get(IRootFolder::class);
 			$this->ownerUserFolder = $rootFolder->getUserFolder($this->superShare->getShareOwner());
 			$sourceId = $this->superShare->getNodeId();
-			$ownerNodes = $this->ownerUserFolder->getById($sourceId);
-			/** @var Node|false $ownerNode */
-			$ownerNode = current($ownerNodes);
+			$ownerNode = $this->ownerUserFolder->getFirstNodeById($sourceId);
 			if (!$ownerNode) {
 				$this->storage = new FailedStorage(['exception' => new NotFoundException("File by id $sourceId not found")]);
 				$this->cache = new FailedCache();
diff --git a/apps/files_sharing/tests/Collaboration/ShareRecipientSorterTest.php b/apps/files_sharing/tests/Collaboration/ShareRecipientSorterTest.php
index ed35c81fb7d8a..6e1aa65cea8b5 100644
--- a/apps/files_sharing/tests/Collaboration/ShareRecipientSorterTest.php
+++ b/apps/files_sharing/tests/Collaboration/ShareRecipientSorterTest.php
@@ -77,9 +77,9 @@ public function testSort($data) {
 
 		if ($data['context']['itemType'] === 'files') {
 			$folder->expects($this->once())
-				->method('getById')
+				->method('getFirstNodeById')
 				->with($data['context']['itemId'])
-				->willReturn([$node]);
+				->willReturn($node);
 
 			$this->shareManager->expects($this->once())
 				->method('getAccessList')
@@ -87,7 +87,7 @@ public function testSort($data) {
 				->willReturn($data['accessList']);
 		} else {
 			$folder->expects($this->never())
-				->method('getById');
+				->method('getFirstNodeById');
 			$this->shareManager->expects($this->never())
 				->method('getAccessList');
 		}
@@ -106,8 +106,8 @@ public function testSortNoNodes() {
 			->willReturn($folder);
 
 		$folder->expects($this->once())
-			->method('getById')
-			->willReturn([]);
+			->method('getFirstNodeById')
+			->willReturn(null);
 
 		$user = $this->createMock(IUser::class);
 		$user->expects($this->any())
diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php
index 731650d8aee8a..b9e58d856881e 100644
--- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php
+++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php
@@ -367,6 +367,7 @@ public function testDeleteShareOwner() {
 	 */
 	public function testDeleteShareFileOwner() {
 		$node = $this->getMockBuilder(File::class)->getMock();
+		$node->method('getId')->willReturn(1);
 
 		$share = $this->newShare();
 		$share->setShareOwner($this->currentUser)
@@ -399,6 +400,7 @@ public function testDeleteShareFileOwner() {
 	 */
 	public function testDeleteSharedWithMyGroup() {
 		$node = $this->getMockBuilder(File::class)->getMock();
+		$node->method('getId')->willReturn(1);
 
 		$share = $this->newShare();
 		$share->setShareType(IShare::TYPE_GROUP)
@@ -435,9 +437,9 @@ public function testDeleteSharedWithMyGroup() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with($share->getNodeId())
-			->willReturn([$share->getNode()]);
+			->willReturn($share->getNode());
 
 		$this->shareManager->expects($this->once())
 			->method('deleteFromSelf')
@@ -461,6 +463,7 @@ public function testDeleteSharedWithGroupIDontBelongTo() {
 		$this->expectExceptionMessage('Wrong share ID, share does not exist');
 
 		$node = $this->getMockBuilder(File::class)->getMock();
+		$node->method('getId')->willReturn(42);
 
 		$share = $this->newShare();
 		$share->setShareType(IShare::TYPE_GROUP)
@@ -497,9 +500,9 @@ public function testDeleteSharedWithGroupIDontBelongTo() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with($share->getNodeId())
-			->willReturn([$share->getNode()]);
+			->willReturn($share->getNode());
 
 		$this->shareManager->expects($this->never())
 			->method('deleteFromSelf');
@@ -813,6 +816,10 @@ public function testGetShare(\OCP\Share\IShare $share, array $result) {
 			->with($share->getNodeId())
 			->willReturn([$share->getNode()]);
 
+		$userFolder->method('getFirstNodeById')
+			->with($share->getNodeId())
+			->willReturn($share->getNode());
+
 		$this->rootFolder->method('getUserFolder')
 			->with($this->currentUser)
 			->willReturn($userFolder);
@@ -1497,9 +1504,9 @@ public function testCanAccessShare() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with($share->getNodeId())
-			->willReturn([$file]);
+			->willReturn($file);
 
 		$file->method('getPermissions')
 			->will($this->onConsecutiveCalls(\OCP\Constants::PERMISSION_SHARE, \OCP\Constants::PERMISSION_READ));
@@ -1593,9 +1600,9 @@ public function testCanAccessRoomShare(bool $expected, \OCP\Share\IShare $share,
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with($share->getNodeId())
-			->willReturn([$share->getNode()]);
+			->willReturn($share->getNode());
 
 		if (!$helperAvailable) {
 			$this->appManager->method('isEnabledForUser')
@@ -1947,6 +1954,7 @@ public function testCreateShareLinkNoLinksAllowed() {
 			]);
 
 		$path = $this->getMockBuilder(Folder::class)->getMock();
+		$path->method('getId')->willReturn(42);
 		$storage = $this->createMock(Storage::class);
 		$storage->method('instanceOfStorage')
 			->willReturnMap([
@@ -1970,6 +1978,7 @@ public function testCreateShareLinkNoPublicUpload() {
 		$this->expectExceptionMessage('Public upload disabled by the administrator');
 
 		$path = $this->getMockBuilder(Folder::class)->getMock();
+		$path->method('getId')->willReturn(42);
 		$storage = $this->createMock(Storage::class);
 		$storage->method('instanceOfStorage')
 			->willReturnMap([
@@ -1994,6 +2003,7 @@ public function testCreateShareLinkPublicUploadFile() {
 		$this->expectExceptionMessage('Public upload is only possible for publicly shared folders');
 
 		$path = $this->getMockBuilder(File::class)->getMock();
+		$path->method('getId')->willReturn(42);
 		$storage = $this->createMock(Storage::class);
 		$storage->method('instanceOfStorage')
 			->willReturnMap([
@@ -2017,6 +2027,7 @@ public function testCreateShareLinkPublicUploadFolder() {
 		$ocs = $this->mockFormatShare();
 
 		$path = $this->getMockBuilder(Folder::class)->getMock();
+		$path->method('getId')->willReturn(1);
 		$storage = $this->createMock(Storage::class);
 		$storage->method('instanceOfStorage')
 			->willReturnMap([
@@ -2055,6 +2066,7 @@ public function testCreateShareLinkPassword() {
 		$ocs = $this->mockFormatShare();
 
 		$path = $this->getMockBuilder(Folder::class)->getMock();
+		$path->method('getId')->willReturn(42);
 		$storage = $this->createMock(Storage::class);
 		$storage->method('instanceOfStorage')
 			->willReturnMap([
@@ -2093,6 +2105,7 @@ public function testCreateShareLinkSendPasswordByTalk() {
 		$ocs = $this->mockFormatShare();
 
 		$path = $this->getMockBuilder(Folder::class)->getMock();
+		$path->method('getId')->willReturn(42);
 		$storage = $this->createMock(Storage::class);
 		$storage->method('instanceOfStorage')
 			->willReturnMap([
@@ -2138,6 +2151,7 @@ public function testCreateShareLinkSendPasswordByTalkWithTalkDisabled() {
 		$ocs = $this->mockFormatShare();
 
 		$path = $this->getMockBuilder(Folder::class)->getMock();
+		$path->method('getId')->willReturn(42);
 		$storage = $this->createMock(Storage::class);
 		$storage->method('instanceOfStorage')
 			->willReturnMap([
@@ -2176,6 +2190,7 @@ public function testCreateShareValidExpireDate() {
 			]);
 
 		$path = $this->getMockBuilder(Folder::class)->getMock();
+		$path->method('getId')->willReturn(42);
 		$storage = $this->createMock(Storage::class);
 		$storage->method('instanceOfStorage')
 			->willReturnMap([
@@ -2221,6 +2236,7 @@ public function testCreateShareInvalidExpireDate() {
 		$ocs = $this->mockFormatShare();
 
 		$path = $this->getMockBuilder(Folder::class)->getMock();
+		$path->method('getId')->willReturn(42);
 		$storage = $this->createMock(Storage::class);
 		$storage->method('instanceOfStorage')
 			->willReturnMap([
@@ -2579,6 +2595,8 @@ public function testCreateReshareOfFederatedMountNoDeletePermissions() {
 			->willReturn($userFolder);
 
 		$path = $this->getMockBuilder(Folder::class)->getMock();
+		$path->method('getId')->willReturn(42);
+
 		$storage = $this->createMock(Storage::class);
 		$storage->method('instanceOfStorage')
 			->willReturnMap([
@@ -2628,9 +2646,9 @@ public function testUpdateShareCantAccess() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with($share->getNodeId())
-			->willReturn([$share->getNode()]);
+			->willReturn($share->getNode());
 
 		$this->ocs->updateShare(42);
 	}
@@ -2721,9 +2739,9 @@ public function testUpdateLinkShareClear() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$node]);
+			->willReturn($node);
 
 		$mountPoint = $this->createMock(IMountPoint::class);
 		$node->method('getMountPoint')
@@ -2775,9 +2793,9 @@ public function testUpdateLinkShareSet() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$folder]);
+			->willReturn($folder);
 
 		$mountPoint = $this->createMock(IMountPoint::class);
 		$folder->method('getMountPoint')
@@ -2825,9 +2843,9 @@ public function testUpdateLinkShareEnablePublicUpload($permissions, $publicUploa
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$folder]);
+			->willReturn($folder);
 
 		$mountPoint = $this->createMock(IMountPoint::class);
 		$folder->method('getMountPoint')
@@ -2883,9 +2901,9 @@ public function testUpdateLinkShareSetCRUDPermissions($permissions) {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$folder]);
+			->willReturn($folder);
 
 		$mountPoint = $this->createMock(IMountPoint::class);
 		$folder->method('getMountPoint')
@@ -2941,9 +2959,9 @@ public function testUpdateLinkShareInvalidDate() {
 
 		$ocs = $this->mockFormatShare();
 		[$userFolder, $folder] = $this->getNonSharedUserFolder();
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$folder]);
+			->willReturn($folder);
 		$this->rootFolder->method('getUserFolder')
 			->with($this->currentUser)
 			->willReturn($userFolder);
@@ -2988,9 +3006,9 @@ public function testUpdateLinkSharePublicUploadNotAllowed($permissions, $publicU
 
 		$ocs = $this->mockFormatShare();
 		[$userFolder, $folder] = $this->getNonSharedUserFolder();
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$folder]);
+			->willReturn($folder);
 		$this->rootFolder->method('getUserFolder')
 			->with($this->currentUser)
 			->willReturn($userFolder);
@@ -3020,9 +3038,9 @@ public function testUpdateLinkSharePublicUploadOnFile() {
 		$file->method('getId')
 			->willReturn(42);
 		[$userFolder, $folder] = $this->getNonSharedUserFolder();
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$folder]);
+			->willReturn($folder);
 		$this->rootFolder->method('getUserFolder')
 			->with($this->currentUser)
 			->willReturn($userFolder);
@@ -3047,9 +3065,9 @@ public function testUpdateLinkSharePasswordDoesNotChangeOther(): void {
 
 		[$userFolder, $node] = $this->getNonSharedUserFolder();
 		$node->method('getId')->willReturn(42);
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$node]);
+			->willReturn($node);
 		$this->rootFolder->method('getUserFolder')
 			->with($this->currentUser)
 			->willReturn($userFolder);
@@ -3098,9 +3116,9 @@ public function testUpdateLinkShareSendPasswordByTalkDoesNotChangeOther() {
 		$date->setTime(0, 0, 0);
 
 		[$userFolder, $node] = $this->getNonSharedUserFolder();
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$node]);
+			->willReturn($node);
 		$this->rootFolder->method('getUserFolder')
 			->with($this->currentUser)
 			->willReturn($userFolder);
@@ -3156,9 +3174,9 @@ public function testUpdateLinkShareSendPasswordByTalkWithTalkDisabledDoesNotChan
 		$date->setTime(0, 0, 0);
 
 		[$userFolder, $node] = $this->getNonSharedUserFolder();
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$node]);
+			->willReturn($node);
 		$this->rootFolder->method('getUserFolder')
 			->with($this->currentUser)
 			->willReturn($userFolder);
@@ -3196,9 +3214,9 @@ public function testUpdateLinkShareDoNotSendPasswordByTalkDoesNotChangeOther() {
 		$date->setTime(0, 0, 0);
 
 		[$userFolder, $node] = $this->getNonSharedUserFolder();
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$node]);
+			->willReturn($node);
 		$this->rootFolder->method('getUserFolder')
 			->with($this->currentUser)
 			->willReturn($userFolder);
@@ -3290,9 +3308,9 @@ public function testUpdateLinkShareDoNotSendPasswordByTalkWithTalkDisabledDoesNo
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$node]);
+			->willReturn($node);
 
 		$mountPoint = $this->createMock(IMountPoint::class);
 		$node->method('getMountPoint')
@@ -3358,9 +3376,9 @@ public function testUpdateLinkShareExpireDateDoesNotChangeOther() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$node]);
+			->willReturn($node);
 
 		$mountPoint = $this->createMock(IMountPoint::class);
 		$node->method('getMountPoint')
@@ -3419,9 +3437,9 @@ public function testUpdateLinkSharePublicUploadDoesNotChangeOther() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$folder]);
+			->willReturn($folder);
 
 		$mountPoint = $this->createMock(IMountPoint::class);
 		$folder->method('getMountPoint')
@@ -3479,9 +3497,9 @@ public function testUpdateLinkSharePermissions() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$folder]);
+			->willReturn($folder);
 
 		$mountPoint = $this->createMock(IMountPoint::class);
 		$folder->method('getMountPoint')
@@ -3537,9 +3555,9 @@ public function testUpdateLinkSharePermissionsShare() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$folder]);
+			->willReturn($folder);
 
 		$mountPoint = $this->createMock(IMountPoint::class);
 		$folder->method('getMountPoint')
@@ -3585,9 +3603,9 @@ public function testUpdateOtherPermissions() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$file]);
+			->willReturn($file);
 
 		$mountPoint = $this->createMock(IMountPoint::class);
 		$file->method('getMountPoint')
@@ -3651,9 +3669,9 @@ public function testUpdateShareCannotIncreasePermissions() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$folder]);
+			->willReturn($folder);
 
 		$mountPoint = $this->createMock(IMountPoint::class);
 		$folder->method('getMountPoint')
@@ -3721,9 +3739,9 @@ public function testUpdateShareCanIncreasePermissionsIfOwner() {
 			->with($this->currentUser)
 			->willReturn($userFolder);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(42)
-			->willReturn([$folder]);
+			->willReturn($folder);
 
 		$mountPoint = $this->createMock(IMountPoint::class);
 		$folder->method('getMountPoint')
@@ -4651,9 +4669,9 @@ public function testFormatShare(array $expects, \OCP\Share\IShare $share, array
 		$this->dateTimeZone->method('getTimezone')->willReturn(new \DateTimeZone('UTC'));
 
 		if (!$exception) {
-			$this->rootFolder->method('getById')
+			$this->rootFolder->method('getFirstNodeById')
 				->with($share->getNodeId())
-				->willReturn([$share->getNode()]);
+				->willReturn($share->getNode());
 
 			$this->rootFolder->method('getRelativePath')
 				->with($share->getNode()->getPath())
@@ -4846,9 +4864,9 @@ public function testFormatRoomShare(array $expects, \OCP\Share\IShare $share, bo
 			->with($this->currentUser)
 			->willReturnSelf();
 
-		$this->rootFolder->method('getById')
+		$this->rootFolder->method('getFirstNodeById')
 			->with($share->getNodeId())
-			->willReturn([$share->getNode()]);
+			->willReturn($share->getNode());
 
 		$this->rootFolder->method('getRelativePath')
 			->with($share->getNode()->getPath())
@@ -4890,6 +4908,7 @@ private function getNonSharedUserFolder(): array {
 			]);
 		$userFolder->method('getStorage')->willReturn($storage);
 		$node->method('getStorage')->willReturn($storage);
+		$node->method('getId')->willReturn(42);
 		return [$userFolder, $node];
 	}
 
@@ -4904,6 +4923,7 @@ private function getNonSharedUserFile(): array {
 			]);
 		$userFolder->method('getStorage')->willReturn($storage);
 		$node->method('getStorage')->willReturn($storage);
+		$node->method('getId')->willReturn(42);
 		return [$userFolder, $node];
 	}
 }
diff --git a/apps/files_sharing/tests/Controller/ShareControllerTest.php b/apps/files_sharing/tests/Controller/ShareControllerTest.php
index 3a73862bf77d5..c8dc6c5de6623 100644
--- a/apps/files_sharing/tests/Controller/ShareControllerTest.php
+++ b/apps/files_sharing/tests/Controller/ShareControllerTest.php
@@ -41,6 +41,7 @@
 use OCA\FederatedFileSharing\FederatedShareProvider;
 use OCA\Files_Sharing\Controller\ShareController;
 use OCA\Files_Sharing\DefaultPublicShareTemplateProvider;
+use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
 use OCP\Accounts\IAccount;
 use OCP\Accounts\IAccountManager;
 use OCP\Accounts\IAccountProperty;
@@ -258,6 +259,7 @@ public function testShowShare() {
 		$file->method('getSize')->willReturn(33);
 		$file->method('isReadable')->willReturn(true);
 		$file->method('isShareable')->willReturn(true);
+		$file->method('getId')->willReturn(111);
 
 		$accountName = $this->createMock(IAccountProperty::class);
 		$accountName->method('getScope')
@@ -330,13 +332,15 @@ public function testShowShare() {
 			return null;
 		});
 
-		$this->eventDispatcher->expects($this->exactly(2))
-			->method('dispatchTyped')
-			->with(
-				$this->callback(function ($event) use ($share) {
+		$this->eventDispatcher->method('dispatchTyped')->with(
+			$this->callback(function ($event) use ($share) {
+				if ($event instanceof BeforeTemplateRenderedEvent) {
 					return $event->getShare() === $share;
-				})
-			);
+				} else {
+					return true;
+				}
+			})
+		);
 
 		$this->l10n->expects($this->any())
 			->method('t')
@@ -416,6 +420,7 @@ public function testShowShareWithPrivateName() {
 		$file->method('getSize')->willReturn(33);
 		$file->method('isReadable')->willReturn(true);
 		$file->method('isShareable')->willReturn(true);
+		$file->method('getId')->willReturn(111);
 
 		$accountName = $this->createMock(IAccountProperty::class);
 		$accountName->method('getScope')
@@ -488,13 +493,15 @@ public function testShowShareWithPrivateName() {
 			return null;
 		});
 
-		$this->eventDispatcher->expects($this->exactly(2))
-			->method('dispatchTyped')
-			->with(
-				$this->callback(function ($event) use ($share) {
+		$this->eventDispatcher->method('dispatchTyped')->with(
+			$this->callback(function ($event) use ($share) {
+				if ($event instanceof BeforeTemplateRenderedEvent) {
 					return $event->getShare() === $share;
-				})
-			);
+				} else {
+					return true;
+				}
+			})
+		);
 
 		$this->l10n->expects($this->any())
 			->method('t')
@@ -574,6 +581,7 @@ public function testShowShareHideDownload() {
 		$file->method('getSize')->willReturn(33);
 		$file->method('isReadable')->willReturn(true);
 		$file->method('isShareable')->willReturn(true);
+		$file->method('getId')->willReturn(111);
 
 		$accountName = $this->createMock(IAccountProperty::class);
 		$accountName->method('getScope')
@@ -650,13 +658,15 @@ public function testShowShareHideDownload() {
 			return null;
 		});
 
-		$this->eventDispatcher->expects($this->exactly(2))
-			->method('dispatchTyped')
-			->with(
-				$this->callback(function ($event) use ($share) {
+		$this->eventDispatcher->method('dispatchTyped')->with(
+			$this->callback(function ($event) use ($share) {
+				if ($event instanceof BeforeTemplateRenderedEvent) {
 					return $event->getShare() === $share;
-				})
-			);
+				} else {
+					return true;
+				}
+			})
+		);
 
 		$this->l10n->expects($this->any())
 			->method('t')
@@ -741,6 +751,7 @@ public function testShareFileDrop() {
 		$folder->method('getStorage')->willReturn($storage);
 		$folder->method('get')->with('')->willReturn($folder);
 		$folder->method('getSize')->willReturn(1337);
+		$folder->method('getId')->willReturn(111);
 
 		$accountName = $this->createMock(IAccountProperty::class);
 		$accountName->method('getScope')
diff --git a/apps/files_sharing/tests/SharedStorageTest.php b/apps/files_sharing/tests/SharedStorageTest.php
index 8e69c5a0f659e..5209a30634a6d 100644
--- a/apps/files_sharing/tests/SharedStorageTest.php
+++ b/apps/files_sharing/tests/SharedStorageTest.php
@@ -586,6 +586,7 @@ public function testInitWithNonExistingUser() {
 	public function testInitWithNotFoundSource() {
 		$share = $this->createMock(IShare::class);
 		$share->method('getShareOwner')->willReturn(self::TEST_FILES_SHARING_API_USER1);
+		$share->method('getNodeId')->willReturn(1);
 		$ownerView = $this->createMock(View::class);
 		$ownerView->method('getPath')->will($this->throwException(new NotFoundException()));
 		$storage = new SharedStorage([
diff --git a/apps/files_trashbin/lib/Trash/LegacyTrashBackend.php b/apps/files_trashbin/lib/Trash/LegacyTrashBackend.php
index 3e749169ad257..ada42dbe857fb 100644
--- a/apps/files_trashbin/lib/Trash/LegacyTrashBackend.php
+++ b/apps/files_trashbin/lib/Trash/LegacyTrashBackend.php
@@ -28,6 +28,7 @@
 use OCA\Files_Trashbin\Storage;
 use OCA\Files_Trashbin\Trashbin;
 use OCP\Files\FileInfo;
+use OCP\Files\Folder;
 use OCP\Files\IRootFolder;
 use OCP\Files\NotFoundException;
 use OCP\Files\Storage\IStorage;
@@ -121,11 +122,11 @@ public function getTrashNodeById(IUser $user, int $fileId) {
 		try {
 			$userFolder = $this->rootFolder->getUserFolder($user->getUID());
 			$trash = $userFolder->getParent()->get('files_trashbin/files');
-			$trashFiles = $trash->getById($fileId);
-			if (!$trashFiles) {
+			if ($trash instanceof Folder) {
+				return $trash->getFirstNodeById($fileId);
+			} else {
 				return null;
 			}
-			return $trashFiles ? array_pop($trashFiles) : null;
 		} catch (NotFoundException $e) {
 			return null;
 		}
diff --git a/apps/files_versions/lib/Sabre/VersionRoot.php b/apps/files_versions/lib/Sabre/VersionRoot.php
index 69ac12ed8e958..2ae1bf04203ef 100644
--- a/apps/files_versions/lib/Sabre/VersionRoot.php
+++ b/apps/files_versions/lib/Sabre/VersionRoot.php
@@ -75,14 +75,12 @@ public function getChild($name) {
 		$userFolder = $this->rootFolder->getUserFolder($this->user->getUID());
 
 		$fileId = (int)$name;
-		$nodes = $userFolder->getById($fileId);
+		$node = $userFolder->getFirstNodeById($fileId);
 
-		if ($nodes === []) {
+		if (!$node) {
 			throw new NotFound();
 		}
 
-		$node = array_pop($nodes);
-
 		if (!$node instanceof File) {
 			throw new NotFound();
 		}
diff --git a/apps/files_versions/lib/Storage.php b/apps/files_versions/lib/Storage.php
index 07bd6b164d273..a35f151d956ef 100644
--- a/apps/files_versions/lib/Storage.php
+++ b/apps/files_versions/lib/Storage.php
@@ -209,9 +209,9 @@ public static function store($filename) {
 		$mount = $file->getMountPoint();
 		if ($mount instanceof SharedMount) {
 			$ownerFolder = $rootFolder->getUserFolder($mount->getShare()->getShareOwner());
-			$ownerNodes = $ownerFolder->getById($file->getId());
-			if (count($ownerNodes)) {
-				$file = current($ownerNodes);
+			$ownerNode = $ownerFolder->getFirstNodeById($file->getId());
+			if ($ownerNode) {
+				$file = $ownerNode;
 				$uid = $mount->getShare()->getShareOwner();
 			}
 		}
diff --git a/apps/files_versions/lib/Versions/LegacyVersionsBackend.php b/apps/files_versions/lib/Versions/LegacyVersionsBackend.php
index a6bf6c2cb1a89..4c365ed4c14b4 100644
--- a/apps/files_versions/lib/Versions/LegacyVersionsBackend.php
+++ b/apps/files_versions/lib/Versions/LegacyVersionsBackend.php
@@ -87,8 +87,7 @@ public function getVersionsForFile(IUser $user, FileInfo $file): array {
 
 			$userFolder = $this->rootFolder->getUserFolder($user->getUID());
 
-			$nodes = $userFolder->getById($fileId);
-			$file = array_pop($nodes);
+			$file = $userFolder->getFirstNodeById($fileId);
 
 			if (!$file) {
 				throw new NotFoundException("version file not found for share owner");
diff --git a/core/Command/FilesMetadata/Get.php b/core/Command/FilesMetadata/Get.php
index 99bc167f71d1e..d1def992c8a78 100644
--- a/core/Command/FilesMetadata/Get.php
+++ b/core/Command/FilesMetadata/Get.php
@@ -96,12 +96,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
 		}
 
 		if ($input->getOption('refresh')) {
-			$node = $this->rootFolder->getUserFolder($input->getArgument('userId'))->getById($fileId);
-			if (count($node) === 0) {
+			$node = $this->rootFolder->getUserFolder($input->getArgument('userId'))->getFirstNodeById($fileId);
+			if (!$node) {
 				throw new NotFoundException();
 			}
 			$metadata = $this->filesMetadataManager->refreshMetadata(
-				$node[0],
+				$node,
 				IFilesMetadataManager::PROCESS_LIVE | IFilesMetadataManager::PROCESS_BACKGROUND
 			);
 		} else {
diff --git a/core/Command/Info/FileUtils.php b/core/Command/Info/FileUtils.php
index 694f30f78b470..595a0216a5ccf 100644
--- a/core/Command/Info/FileUtils.php
+++ b/core/Command/Info/FileUtils.php
@@ -87,11 +87,7 @@ public function getNode(string $fileInput): ?Node {
 			}
 			$mount = $mounts[0];
 			$userFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID());
-			$nodes = $userFolder->getById((int)$fileInput);
-			if (!$nodes) {
-				return null;
-			}
-			return $nodes[0];
+			return $userFolder->getFirstNodeById((int)$fileInput);
 		} else {
 			try {
 				return $this->rootFolder->get($fileInput);
diff --git a/core/Command/Preview/Generate.php b/core/Command/Preview/Generate.php
index a885d224fc7e7..865283191993b 100644
--- a/core/Command/Preview/Generate.php
+++ b/core/Command/Preview/Generate.php
@@ -121,11 +121,7 @@ private function getFile(string $fileInput): ?Node {
 			}
 			$mount = $mounts[0];
 			$userFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID());
-			$nodes = $userFolder->getById((int)$fileInput);
-			if (!$nodes) {
-				return null;
-			}
-			return $nodes[0];
+			return $userFolder->getFirstNodeById((int)$fileInput);
 		} else {
 			try {
 				return $this->rootFolder->get($fileInput);
diff --git a/core/Controller/PreviewController.php b/core/Controller/PreviewController.php
index 34c21bd3ecb9e..7ab22dceaa2a4 100644
--- a/core/Controller/PreviewController.php
+++ b/core/Controller/PreviewController.php
@@ -133,14 +133,12 @@ public function getPreviewByFileId(
 		}
 
 		$userFolder = $this->root->getUserFolder($this->userId);
-		$nodes = $userFolder->getById($fileId);
+		$node = $userFolder->getFirstNodeById($fileId);
 
-		if (\count($nodes) === 0) {
+		if (!$node) {
 			return new DataResponse([], Http::STATUS_NOT_FOUND);
 		}
 
-		$node = array_pop($nodes);
-
 		return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
 	}
 
diff --git a/lib/private/Collaboration/Reference/File/FileReferenceProvider.php b/lib/private/Collaboration/Reference/File/FileReferenceProvider.php
index 5f384213976f4..125649246df74 100644
--- a/lib/private/Collaboration/Reference/File/FileReferenceProvider.php
+++ b/lib/private/Collaboration/Reference/File/FileReferenceProvider.php
@@ -31,7 +31,6 @@
 use OCP\Files\IMimeTypeDetector;
 use OCP\Files\InvalidPathException;
 use OCP\Files\IRootFolder;
-use OCP\Files\Node;
 use OCP\Files\NotFoundException;
 use OCP\Files\NotPermittedException;
 use OCP\IL10N;
@@ -121,15 +120,12 @@ private function fetchReference(Reference $reference): void {
 
 		try {
 			$userFolder = $this->rootFolder->getUserFolder($this->userId);
-			$files = $userFolder->getById($fileId);
+			$file = $userFolder->getFirstNodeById($fileId);
 
-			if (empty($files)) {
+			if (!$file) {
 				throw new NotFoundException();
 			}
 
-			/** @var Node $file */
-			$file = array_shift($files);
-
 			$reference->setTitle($file->getName());
 			$reference->setDescription($file->getMimetype());
 			$reference->setUrl($this->urlGenerator->getAbsoluteURL('/index.php/f/' . $fileId));
diff --git a/lib/private/DirectEditing/Manager.php b/lib/private/DirectEditing/Manager.php
index d1be1f50330c9..da4811589dadc 100644
--- a/lib/private/DirectEditing/Manager.php
+++ b/lib/private/DirectEditing/Manager.php
@@ -310,11 +310,11 @@ public function getFileForToken($userId, $fileId, $filePath = null): Node {
 		if ($filePath !== null) {
 			return $userFolder->get($filePath);
 		}
-		$files = $userFolder->getById($fileId);
-		if (count($files) === 0) {
+		$file = $userFolder->getFirstNodeById($fileId);
+		if (!$file) {
 			throw new NotFoundException('File nound found by id ' . $fileId);
 		}
-		return $files[0];
+		return $file;
 	}
 
 	public function isEnabled(): bool {
diff --git a/lib/private/Files/Config/CachedMountInfo.php b/lib/private/Files/Config/CachedMountInfo.php
index 7c97135a56595..19fa87aa0909d 100644
--- a/lib/private/Files/Config/CachedMountInfo.php
+++ b/lib/private/Files/Config/CachedMountInfo.php
@@ -97,12 +97,7 @@ public function getMountPointNode(): ?Node {
 		// TODO injection etc
 		Filesystem::initMountPoints($this->getUser()->getUID());
 		$userNode = \OC::$server->getUserFolder($this->getUser()->getUID());
-		$nodes = $userNode->getParent()->getById($this->getRootId());
-		if (count($nodes) > 0) {
-			return $nodes[0];
-		} else {
-			return null;
-		}
+		return $userNode->getParent()->getFirstNodeById($this->getRootId());
 	}
 
 	/**
diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php
index cb8747e0055d9..014b66fdf63ba 100644
--- a/lib/private/Files/Node/Folder.php
+++ b/lib/private/Files/Node/Folder.php
@@ -314,7 +314,7 @@ public function getById($id) {
 	}
 
 	public function getFirstNodeById(int $id): ?\OCP\Files\Node {
-		return current($this->getById($id));
+		return current($this->getById($id)) ?: null;
 	}
 
 	protected function getAppDataDirectoryName(): string {
diff --git a/lib/private/FilesMetadata/Job/UpdateSingleMetadata.php b/lib/private/FilesMetadata/Job/UpdateSingleMetadata.php
index d18c8aa368061..3a3b35ce205dd 100644
--- a/lib/private/FilesMetadata/Job/UpdateSingleMetadata.php
+++ b/lib/private/FilesMetadata/Job/UpdateSingleMetadata.php
@@ -55,10 +55,9 @@ protected function run($argument) {
 		[$userId, $fileId] = $argument;
 
 		try {
-			$node = $this->rootFolder->getUserFolder($userId)->getById($fileId);
-			if (count($node) > 0) {
-				$file = array_shift($node);
-				$this->filesMetadataManager->refreshMetadata($file, IFilesMetadataManager::PROCESS_BACKGROUND);
+			$node = $this->rootFolder->getUserFolder($userId)->getFirstNodeById($fileId);
+			if ($node) {
+				$this->filesMetadataManager->refreshMetadata($node, IFilesMetadataManager::PROCESS_BACKGROUND);
 			}
 		} catch (\Exception $e) {
 			$this->logger->warning('issue while running UpdateSingleMetadata', ['exception' => $e, 'userId' => $userId, 'fileId' => $fileId]);
diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php
index 53f88cf14ce50..c1abaff2ec026 100644
--- a/lib/private/Share20/Manager.php
+++ b/lib/private/Share20/Manager.php
@@ -311,8 +311,7 @@ protected function generalCreateChecks(IShare $share) {
 			$mount = $userMount->getMountPoint();
 			// When it's a reshare use the parent share permissions as maximum
 			$userMountPointId = $mount->getStorageRootId();
-			$userMountPoints = $userFolder->getById($userMountPointId);
-			$userMountPoint = array_shift($userMountPoints);
+			$userMountPoint = $userFolder->getFirstNodeById($userMountPointId);
 
 			if ($userMountPoint === null) {
 				throw new GenericShareException('Could not get proper user mount for ' . $userMountPointId . '. Failing since else the next calls are called with null');
@@ -1723,8 +1722,7 @@ public function getAccessList(\OCP\Files\Node $path, $recursive = true, $current
 		//Get node for the owner and correct the owner in case of external storage
 		$userFolder = $this->rootFolder->getUserFolder($owner);
 		if ($path->getId() !== $userFolder->getId() && !$userFolder->isSubNode($path)) {
-			$nodes = $userFolder->getById($path->getId());
-			$path = array_shift($nodes);
+			$path = $userFolder->getFirstNodeById($path->getId());
 			if ($path === null || $path->getOwner() === null) {
 				return [];
 			}
diff --git a/lib/private/Share20/Share.php b/lib/private/Share20/Share.php
index c80d332e9dba2..19b36cb60e81e 100644
--- a/lib/private/Share20/Share.php
+++ b/lib/private/Share20/Share.php
@@ -188,12 +188,12 @@ public function getNode() {
 				$userFolder = $this->rootFolder->getUserFolder($this->sharedBy);
 			}
 
-			$nodes = $userFolder->getById($this->fileId);
-			if (empty($nodes)) {
+			$node = $userFolder->getFirstNodeById($this->fileId);
+			if (!$node) {
 				throw new NotFoundException('Node for share not found, fileid: ' . $this->fileId);
 			}
 
-			$this->node = $nodes[0];
+			$this->node = $node;
 		}
 
 		return $this->node;
@@ -211,12 +211,16 @@ public function setNodeId($fileId) {
 	/**
 	 * @inheritdoc
 	 */
-	public function getNodeId() {
+	public function getNodeId(): int {
 		if ($this->fileId === null) {
 			$this->fileId = $this->getNode()->getId();
 		}
 
-		return $this->fileId;
+		if ($this->fileId === null) {
+			throw new NotFoundException("Share source not found");
+		} else {
+			return $this->fileId;
+		}
 	}
 
 	/**
diff --git a/lib/private/SpeechToText/TranscriptionJob.php b/lib/private/SpeechToText/TranscriptionJob.php
index 8921d52ecd15d..083cd129657e6 100644
--- a/lib/private/SpeechToText/TranscriptionJob.php
+++ b/lib/private/SpeechToText/TranscriptionJob.php
@@ -65,7 +65,7 @@ protected function run($argument) {
 		try {
 			\OC_Util::setupFS($owner);
 			$userFolder = $this->rootFolder->getUserFolder($owner);
-			$file = current($userFolder->getById($fileId));
+			$file = $userFolder->getFirstNodeById($fileId);
 			if (!($file instanceof File)) {
 				$this->logger->warning('Transcription of file ' . $fileId . ' failed. The file could not be found');
 				$this->eventDispatcher->dispatchTyped(
diff --git a/lib/public/Share/IShare.php b/lib/public/Share/IShare.php
index 74d404101cd03..a059696a75e93 100644
--- a/lib/public/Share/IShare.php
+++ b/lib/public/Share/IShare.php
@@ -213,7 +213,7 @@ public function setNodeId($fileId);
 	 * @since 9.0.0
 	 * @throws NotFoundException
 	 */
-	public function getNodeId();
+	public function getNodeId(): int;
 
 	/**
 	 * Set the type of node (file/folder)
diff --git a/tests/lib/Share20/DefaultShareProviderTest.php b/tests/lib/Share20/DefaultShareProviderTest.php
index f977619b7b246..b658e20d83788 100644
--- a/tests/lib/Share20/DefaultShareProviderTest.php
+++ b/tests/lib/Share20/DefaultShareProviderTest.php
@@ -210,7 +210,7 @@ public function testGetShareByIdUserShare() {
 
 		$ownerPath = $this->createMock(File::class);
 		$shareOwnerFolder = $this->createMock(Folder::class);
-		$shareOwnerFolder->method('getById')->with(42)->willReturn([$ownerPath]);
+		$shareOwnerFolder->method('getFirstNodeById')->with(42)->willReturn($ownerPath);
 
 		$this->rootFolder
 			->method('getUserFolder')
@@ -288,7 +288,7 @@ public function testGetShareByIdLazy2() {
 		$ownerPath = $this->createMock(File::class);
 
 		$shareOwnerFolder = $this->createMock(Folder::class);
-		$shareOwnerFolder->method('getById')->with(42)->willReturn([$ownerPath]);
+		$shareOwnerFolder->method('getFirstNodeById')->with(42)->willReturn($ownerPath);
 
 		$this->rootFolder
 			->method('getUserFolder')
@@ -332,7 +332,7 @@ public function testGetShareByIdGroupShare() {
 
 		$ownerPath = $this->createMock(Folder::class);
 		$shareOwnerFolder = $this->createMock(Folder::class);
-		$shareOwnerFolder->method('getById')->with(42)->willReturn([$ownerPath]);
+		$shareOwnerFolder->method('getFirstNodeById')->with(42)->willReturn($ownerPath);
 
 		$this->rootFolder
 				->method('getUserFolder')
@@ -371,7 +371,7 @@ public function testGetShareByIdUserGroupShare() {
 		$node->method('getId')->willReturn(42);
 
 		$this->rootFolder->method('getUserFolder')->with('user0')->willReturnSelf();
-		$this->rootFolder->method('getById')->willReturn([$node]);
+		$this->rootFolder->method('getFirstNodeById')->willReturn($node);
 
 		$this->userManager->method('get')->willReturnMap([
 			['user0', $user0],
@@ -416,7 +416,7 @@ public function testGetShareByIdLinkShare() {
 
 		$ownerPath = $this->createMock(Folder::class);
 		$shareOwnerFolder = $this->createMock(Folder::class);
-		$shareOwnerFolder->method('getById')->with(42)->willReturn([$ownerPath]);
+		$shareOwnerFolder->method('getFirstNodeById')->with(42)->willReturn($ownerPath);
 
 		$this->rootFolder
 				->method('getUserFolder')
@@ -633,7 +633,7 @@ public function testGetChildren() {
 
 		$ownerPath = $this->createMock(Folder::class);
 		$ownerFolder = $this->createMock(Folder::class);
-		$ownerFolder->method('getById')->willReturn([$ownerPath]);
+		$ownerFolder->method('getFirstNodeById')->willReturn($ownerPath);
 
 		$this->rootFolder
 			->method('getUserFolder')
@@ -690,12 +690,12 @@ public function testCreateUserShare() {
 				['shareOwner', $ownerFolder],
 			]);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(100)
-			->willReturn([$path]);
-		$ownerFolder->method('getById')
+			->willReturn($path);
+		$ownerFolder->method('getFirstNodeById')
 			->with(100)
-			->willReturn([$path]);
+			->willReturn($path);
 
 		$share->setShareType(IShare::TYPE_USER);
 		$share->setSharedWith('sharedWith');
@@ -762,12 +762,12 @@ public function testCreateGroupShare() {
 				['shareOwner', $ownerFolder],
 			]);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with(100)
-			->willReturn([$path]);
-		$ownerFolder->method('getById')
+			->willReturn($path);
+		$ownerFolder->method('getFirstNodeById')
 			->with(100)
-			->willReturn([$path]);
+			->willReturn($path);
 
 		$share->setShareType(IShare::TYPE_GROUP);
 		$share->setSharedWith('sharedWith');
@@ -832,12 +832,12 @@ public function testCreateLinkShare() {
 					['shareOwner', $ownerFolder],
 				]);
 
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 				->with(100)
-				->willReturn([$path]);
-		$ownerFolder->method('getById')
+				->willReturn($path);
+		$ownerFolder->method('getFirstNodeById')
 				->with(100)
-				->willReturn([$path]);
+				->willReturn($path);
 
 		$share->setShareType(IShare::TYPE_LINK);
 		$share->setSharedBy('sharedBy');
@@ -890,7 +890,7 @@ public function testGetShareByToken() {
 		$file = $this->createMock(File::class);
 
 		$this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf();
-		$this->rootFolder->method('getById')->with(42)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with(42)->willReturn($file);
 
 		$share = $this->provider->getShareByToken('secrettoken');
 		$this->assertEquals($id, $share->getId());
@@ -981,7 +981,7 @@ public function testGetSharedWithUser($storageStringId, $fileName1, $fileName2)
 
 		$file = $this->createMock(File::class);
 		$this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf();
-		$this->rootFolder->method('getById')->with($fileId)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with($fileId)->willReturn($file);
 
 		$share = $this->provider->getSharedWith('sharedWith', IShare::TYPE_USER, null, 1, 0);
 		$this->assertCount(1, $share);
@@ -1053,7 +1053,7 @@ public function testGetSharedWithGroup($storageStringId, $fileName1, $fileName2)
 
 		$file = $this->createMock(File::class);
 		$this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf();
-		$this->rootFolder->method('getById')->with($fileId)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with($fileId)->willReturn($file);
 
 		$share = $this->provider->getSharedWith('sharedWith', IShare::TYPE_GROUP, null, 20, 1);
 		$this->assertCount(1, $share);
@@ -1141,7 +1141,7 @@ public function testGetSharedWithGroupUserModified($storageStringId, $fileName1,
 
 		$file = $this->createMock(File::class);
 		$this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf();
-		$this->rootFolder->method('getById')->with($fileId)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with($fileId)->willReturn($file);
 
 		$share = $this->provider->getSharedWith('user', IShare::TYPE_GROUP, null, -1, 0);
 		$this->assertCount(1, $share);
@@ -1184,7 +1184,7 @@ public function testGetSharedWithUserWithNode($storageStringId, $fileName1, $fil
 		$file->method('getId')->willReturn($fileId2);
 
 		$this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf();
-		$this->rootFolder->method('getById')->with($fileId2)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with($fileId2)->willReturn($file);
 
 		$share = $this->provider->getSharedWith('user0', IShare::TYPE_USER, $file, -1, 0);
 		$this->assertCount(1, $share);
@@ -1225,7 +1225,7 @@ public function testGetSharedWithGroupWithNode($storageStringId, $fileName1, $fi
 		$node = $this->createMock(Folder::class);
 		$node->method('getId')->willReturn($fileId2);
 		$this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf();
-		$this->rootFolder->method('getById')->with($fileId2)->willReturn([$node]);
+		$this->rootFolder->method('getFirstNodeById')->with($fileId2)->willReturn($node);
 
 		$share = $this->provider->getSharedWith('user0', IShare::TYPE_GROUP, $node, -1, 0);
 		$this->assertCount(1, $share);
@@ -1276,7 +1276,7 @@ public function testGetSharedWithWithDeletedFile($shareType, $trashed) {
 
 		$file = $this->createMock(File::class);
 		$this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf();
-		$this->rootFolder->method('getById')->with($deletedFileId)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with($deletedFileId)->willReturn($file);
 
 		$groups = [];
 		foreach (range(0, 100) as $i) {
@@ -1336,7 +1336,7 @@ public function testGetSharesBy() {
 
 		$file = $this->createMock(File::class);
 		$this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf();
-		$this->rootFolder->method('getById')->with(42)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with(42)->willReturn($file);
 
 		$share = $this->provider->getSharesBy('sharedBy', IShare::TYPE_USER, null, false, 1, 0);
 		$this->assertCount(1, $share);
@@ -1386,7 +1386,7 @@ public function testGetSharesNode() {
 		$file = $this->createMock(File::class);
 		$file->method('getId')->willReturn(42);
 		$this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf();
-		$this->rootFolder->method('getById')->with(42)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with(42)->willReturn($file);
 
 		$share = $this->provider->getSharesBy('sharedBy', IShare::TYPE_USER, $file, false, 1, 0);
 		$this->assertCount(1, $share);
@@ -1436,7 +1436,7 @@ public function testGetSharesReshare() {
 		$file = $this->createMock(File::class);
 		$file->method('getId')->willReturn(42);
 		$this->rootFolder->method('getUserFolder')->with('shareOwner')->willReturnSelf();
-		$this->rootFolder->method('getById')->with(42)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with(42)->willReturn($file);
 
 		$shares = $this->provider->getSharesBy('shareOwner', IShare::TYPE_USER, null, true, -1, 0);
 		$this->assertCount(2, $shares);
@@ -1496,7 +1496,7 @@ public function testDeleteFromSelfGroupNoCustomShare() {
 		$file->method('getId')->willReturn(1);
 
 		$this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf();
-		$this->rootFolder->method('getById')->with(1)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file);
 
 		$share = $this->provider->getShareById($id);
 
@@ -1568,7 +1568,7 @@ public function testDeleteFromSelfGroupAlreadyCustomShare() {
 		$file->method('getId')->willReturn(1);
 
 		$this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf();
-		$this->rootFolder->method('getById')->with(1)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file);
 
 		$share = $this->provider->getShareById($id);
 
@@ -1626,7 +1626,7 @@ public function testDeleteFromSelfGroupUserNotInGroup() {
 		$file->method('getId')->willReturn(1);
 
 		$this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf();
-		$this->rootFolder->method('getById')->with(1)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file);
 
 		$share = $this->provider->getShareById($id);
 
@@ -1668,7 +1668,7 @@ public function testDeleteFromSelfGroupDoesNotExist() {
 		$file->method('getId')->willReturn(1);
 
 		$this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf();
-		$this->rootFolder->method('getById')->with(1)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file);
 
 		$share = $this->provider->getShareById($id);
 
@@ -1706,7 +1706,7 @@ public function testDeleteFromSelfUser() {
 		$file->method('getId')->willReturn(1);
 
 		$this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf();
-		$this->rootFolder->method('getById')->with(1)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file);
 
 		$share = $this->provider->getShareById($id);
 
@@ -1760,7 +1760,7 @@ public function testDeleteFromSelfUserNotRecipient() {
 		$file->method('getId')->willReturn(1);
 
 		$this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf();
-		$this->rootFolder->method('getById')->with(1)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file);
 
 		$share = $this->provider->getShareById($id);
 
@@ -1797,7 +1797,7 @@ public function testDeleteFromSelfLink() {
 		$file->method('getId')->willReturn(1);
 
 		$this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf();
-		$this->rootFolder->method('getById')->with(1)->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->with(1)->willReturn($file);
 
 		$share = $this->provider->getShareById($id);
 
@@ -1828,9 +1828,9 @@ function ($userId) use ($users) {
 		$file2->method('getId')->willReturn(43);
 
 		$folder1 = $this->createMock(Folder::class);
-		$folder1->method('getById')->with(42)->willReturn([$file1]);
+		$folder1->method('getFirstNodeById')->with(42)->willReturn($file1);
 		$folder2 = $this->createMock(Folder::class);
-		$folder2->method('getById')->with(43)->willReturn([$file2]);
+		$folder2->method('getFirstNodeById')->with(43)->willReturn($file2);
 
 		$this->rootFolder->method('getUserFolder')->willReturnMap([
 			['user2', $folder1],
@@ -1885,9 +1885,9 @@ function ($userId) use ($users) {
 		$file2->method('getId')->willReturn(43);
 
 		$folder1 = $this->createMock(Folder::class);
-		$folder1->method('getById')->with(42)->willReturn([$file1]);
+		$folder1->method('getFirstNodeById')->with(42)->willReturn($file1);
 		$folder2 = $this->createMock(Folder::class);
-		$folder2->method('getById')->with(43)->willReturn([$file2]);
+		$folder2->method('getFirstNodeById')->with(43)->willReturn($file2);
 
 		$this->rootFolder->method('getUserFolder')->willReturnMap([
 			['user2', $folder1],
@@ -1951,9 +1951,9 @@ function ($userId) use ($users) {
 		$file2->method('getId')->willReturn(43);
 
 		$folder1 = $this->createMock(Folder::class);
-		$folder1->method('getById')->with(42)->willReturn([$file1]);
+		$folder1->method('getFirstNodeById')->with(42)->willReturn($file1);
 		$folder2 = $this->createMock(Folder::class);
-		$folder2->method('getById')->with(43)->willReturn([$file2]);
+		$folder2->method('getFirstNodeById')->with(43)->willReturn($file2);
 
 		$this->rootFolder->method('getUserFolder')->willReturnMap([
 			['user2', $folder1],
@@ -2022,9 +2022,9 @@ function ($groupId) use ($groups) {
 		$file2->method('getId')->willReturn(43);
 
 		$folder1 = $this->createMock(Folder::class);
-		$folder1->method('getById')->with(42)->willReturn([$file1]);
+		$folder1->method('getFirstNodeById')->with(42)->willReturn($file1);
 		$folder2 = $this->createMock(Folder::class);
-		$folder2->method('getById')->with(43)->willReturn([$file2]);
+		$folder2->method('getFirstNodeById')->with(43)->willReturn($file2);
 
 		$this->rootFolder->method('getUserFolder')->willReturnMap([
 			['user2', $folder1],
@@ -2101,9 +2101,9 @@ function ($groupId) use ($groups) {
 		$file2->method('getId')->willReturn(43);
 
 		$folder1 = $this->createMock(Folder::class);
-		$folder1->method('getById')->with(42)->willReturn([$file1]);
+		$folder1->method('getFirstNodeById')->with(42)->willReturn($file1);
 		$folder2 = $this->createMock(Folder::class);
-		$folder2->method('getById')->with(43)->willReturn([$file2]);
+		$folder2->method('getFirstNodeById')->with(43)->willReturn($file2);
 
 		$this->rootFolder->method('getUserFolder')->willReturnMap([
 			['user2', $folder1],
@@ -2179,7 +2179,7 @@ public function testMoveUserShare() {
 		$file->method('getId')->willReturn(42);
 
 		$this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf();
-		$this->rootFolder->method('getById')->willReturn([$file]);
+		$this->rootFolder->method('getFirstNodeById')->willReturn($file);
 
 		$share = $this->provider->getShareById($id, null);
 
@@ -2215,7 +2215,7 @@ public function testMoveGroupShare() {
 		$folder->method('getId')->willReturn(42);
 
 		$this->rootFolder->method('getUserFolder')->with('user1')->willReturnSelf();
-		$this->rootFolder->method('getById')->willReturn([$folder]);
+		$this->rootFolder->method('getFirstNodeById')->willReturn($folder);
 
 		$share = $this->provider->getShareById($id, 'user0');
 
@@ -2881,23 +2881,23 @@ public function testGetAllShares() {
 
 		$ownerPath1 = $this->createMock(File::class);
 		$shareOwner1Folder = $this->createMock(Folder::class);
-		$shareOwner1Folder->method('getById')->willReturn([$ownerPath1]);
+		$shareOwner1Folder->method('getFirstNodeById')->willReturn($ownerPath1);
 
 		$ownerPath2 = $this->createMock(File::class);
 		$shareOwner2Folder = $this->createMock(Folder::class);
-		$shareOwner2Folder->method('getById')->willReturn([$ownerPath2]);
+		$shareOwner2Folder->method('getFirstNodeById')->willReturn($ownerPath2);
 
 		$ownerPath3 = $this->createMock(File::class);
 		$shareOwner3Folder = $this->createMock(Folder::class);
-		$shareOwner3Folder->method('getById')->willReturn([$ownerPath3]);
+		$shareOwner3Folder->method('getFirstNodeById')->willReturn($ownerPath3);
 
 		$ownerPath4 = $this->createMock(File::class);
 		$shareOwner4Folder = $this->createMock(Folder::class);
-		$shareOwner4Folder->method('getById')->willReturn([$ownerPath4]);
+		$shareOwner4Folder->method('getFirstNodeById')->willReturn($ownerPath4);
 
 		$ownerPath5 = $this->createMock(File::class);
 		$shareOwner5Folder = $this->createMock(Folder::class);
-		$shareOwner5Folder->method('getById')->willReturn([$ownerPath5]);
+		$shareOwner5Folder->method('getFirstNodeById')->willReturn($ownerPath5);
 
 		$this->rootFolder
 			->method('getUserFolder')
diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php
index 25c011825da35..049450133dd58 100644
--- a/tests/lib/Share20/ManagerTest.php
+++ b/tests/lib/Share20/ManagerTest.php
@@ -795,9 +795,9 @@ public function testGeneralChecks($share, $exceptionMessage, $exception) {
 			->willReturn(42);
 		// Id 108 is used in the data to refer to the node of the share.
 		$userFolder->expects($this->any())
-			->method('getById')
+			->method('getFirstNodeById')
 			->with(108)
-			->willReturn([$share->getNode()]);
+			->willReturn($share->getNode());
 		$userFolder->expects($this->any())
 			->method('getRelativePath')
 			->willReturnArgument(0);
@@ -4464,9 +4464,9 @@ public function testGetAccessList() {
 			->willReturn($userFolder);
 		$folder->method('getPath')
 			->willReturn('/owner/files/folder');
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with($this->equalTo(42))
-			->willReturn([12 => $file]);
+			->willReturn($file);
 		$userFolder->method('getPath')
 			->willReturn('/user1/files');
 
@@ -4583,9 +4583,9 @@ public function testGetAccessListWithCurrentAccess() {
 			->willReturn($userFolder);
 		$folder->method('getPath')
 			->willReturn('/owner/files/folder');
-		$userFolder->method('getById')
+		$userFolder->method('getFirstNodeById')
 			->with($this->equalTo(42))
-			->willReturn([42 => $file]);
+			->willReturn($file);
 		$userFolder->method('getPath')
 			->willReturn('/user1/files');
 

From a9ee278ec6059d164ed2f45b0eb304e940ffd588 Mon Sep 17 00:00:00 2001
From: Robin Appelman <robin@icewind.nl>
Date: Fri, 16 Feb 2024 12:34:07 +0100
Subject: [PATCH 3/3] perf: cache path by id to speedup getting nodes by id

Signed-off-by: Robin Appelman <robin@icewind.nl>
---
 lib/private/Files/Node/Root.php            | 30 +++++++++++++-
 lib/private/Server.php                     |  5 ++-
 tests/lib/Files/Node/FileTest.php          | 25 +++++++-----
 tests/lib/Files/Node/FolderTest.php        | 46 +++++++++++-----------
 tests/lib/Files/Node/HookConnectorTest.php | 10 ++++-
 tests/lib/Files/Node/IntegrationTest.php   | 10 ++++-
 tests/lib/Files/Node/NodeTest.php          | 19 +++++++--
 tests/lib/Files/Node/RootTest.php          | 27 ++++++++++---
 8 files changed, 124 insertions(+), 48 deletions(-)

diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php
index bc76640966e43..71bc7a6da6bf0 100644
--- a/lib/private/Files/Node/Root.php
+++ b/lib/private/Files/Node/Root.php
@@ -49,6 +49,8 @@
 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;
@@ -81,6 +83,7 @@ class Root extends Folder implements IRootFolder {
 	private LoggerInterface $logger;
 	private IUserManager $userManager;
 	private IEventDispatcher $eventDispatcher;
+	private ICache $pathByIdCache;
 
 	/**
 	 * @param Manager $manager
@@ -94,7 +97,8 @@ public function __construct(
 		IUserMountCache $userMountCache,
 		LoggerInterface $logger,
 		IUserManager $userManager,
-		IEventDispatcher $eventDispatcher
+		IEventDispatcher $eventDispatcher,
+		ICacheFactory $cacheFactory,
 	) {
 		parent::__construct($this, $view, '');
 		$this->mountManager = $manager;
@@ -107,6 +111,7 @@ public function __construct(
 		$eventDispatcher->addListener(FilesystemTornDownEvent::class, function () {
 			$this->userFolderCache = new CappedMemoryCache();
 		});
+		$this->pathByIdCache = $cacheFactory->createLocal('path-by-id');
 	}
 
 	/**
@@ -406,7 +411,28 @@ public function getUserMountCache() {
 	}
 
 	public function getFirstNodeByIdInPath(int $id, string $path): ?INode {
-		return current($this->getByIdInPath($id, $path));
+		// 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;
 	}
 
 	/**
diff --git a/lib/private/Server.php b/lib/private/Server.php
index 1aedd7d06ac56..c956da4fdd80e 100644
--- a/lib/private/Server.php
+++ b/lib/private/Server.php
@@ -448,14 +448,17 @@ public function __construct($webRoot, \OC\Config $config) {
 		$this->registerService('RootFolder', function (ContainerInterface $c) {
 			$manager = \OC\Files\Filesystem::getMountManager();
 			$view = new View();
+			/** @var IUserSession $userSession */
+			$userSession = $c->get(IUserSession::class);
 			$root = new Root(
 				$manager,
 				$view,
-				null,
+				$userSession->getUser(),
 				$c->get(IUserMountCache::class),
 				$this->get(LoggerInterface::class),
 				$this->get(IUserManager::class),
 				$this->get(IEventDispatcher::class),
+				$this->get(ICacheFactory::class),
 			);
 
 			$previewConnector = new \OC\Preview\WatcherConnector(
diff --git a/tests/lib/Files/Node/FileTest.php b/tests/lib/Files/Node/FileTest.php
index 218e253172746..b245f5e2db540 100644
--- a/tests/lib/Files/Node/FileTest.php
+++ b/tests/lib/Files/Node/FileTest.php
@@ -39,7 +39,7 @@ protected function getViewDeleteMethod() {
 	public function testGetContent() {
 		/** @var \OC\Files\Node\Root|\PHPUnit\Framework\MockObject\MockObject $root */
 		$root = $this->getMockBuilder('\OC\Files\Node\Root')
-			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 
 		$hook = function ($file) {
@@ -69,7 +69,7 @@ public function testGetContentNotPermitted() {
 
 		/** @var \OC\Files\Node\Root|\PHPUnit\Framework\MockObject\MockObject $root */
 		$root = $this->getMockBuilder('\OC\Files\Node\Root')
-			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 
 		$root->expects($this->any())
@@ -88,7 +88,7 @@ public function testGetContentNotPermitted() {
 	public function testPutContent() {
 		/** @var \OC\Files\Node\Root|\PHPUnit\Framework\MockObject\MockObject $root */
 		$root = $this->getMockBuilder('\OC\Files\Node\Root')
-			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 
 		$root->expects($this->any())
@@ -115,7 +115,7 @@ public function testPutContentNotPermitted() {
 
 		/** @var \OC\Files\Node\Root|\PHPUnit\Framework\MockObject\MockObject $root */
 		$root = $this->getMockBuilder('\OC\Files\Node\Root')
-			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 
 		$this->view->expects($this->once())
@@ -130,7 +130,7 @@ public function testPutContentNotPermitted() {
 	public function testGetMimeType() {
 		/** @var \OC\Files\Node\Root|\PHPUnit\Framework\MockObject\MockObject $root */
 		$root = $this->getMockBuilder('\OC\Files\Node\Root')
-			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 
 		$this->view->expects($this->once())
@@ -154,7 +154,8 @@ public function testFOpenRead() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 
 		$hook = function ($file) {
@@ -190,7 +191,8 @@ public function testFOpenWrite() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 		$hooksCalled = 0;
 		$hook = function ($file) use (&$hooksCalled) {
@@ -230,7 +232,8 @@ public function testFOpenReadNotPermitted() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 		$hook = function ($file) {
 			throw new \Exception('Hooks are not supposed to be called');
@@ -256,7 +259,8 @@ public function testFOpenReadWriteNoReadPermissions() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 		$hook = function () {
 			throw new \Exception('Hooks are not supposed to be called');
@@ -282,7 +286,8 @@ public function testFOpenReadWriteNoWritePermissions() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 		$hook = function () {
 			throw new \Exception('Hooks are not supposed to be called');
diff --git a/tests/lib/Files/Node/FolderTest.php b/tests/lib/Files/Node/FolderTest.php
index 0bcf69c5c1303..a219222d008e1 100644
--- a/tests/lib/Files/Node/FolderTest.php
+++ b/tests/lib/Files/Node/FolderTest.php
@@ -68,7 +68,7 @@ public function testGetDirectoryContent() {
 		 * @var \OC\Files\View | \PHPUnit\Framework\MockObject\MockObject $view
 		 */
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->expects($this->any())
 			->method('getUser')
@@ -101,7 +101,7 @@ public function testGet() {
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->expects($this->any())
 			->method('getUser')
@@ -120,7 +120,7 @@ public function testNodeExists() {
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->expects($this->any())
 			->method('getUser')
@@ -140,7 +140,7 @@ public function testNodeExistsNotExists() {
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->expects($this->any())
 			->method('getUser')
@@ -158,7 +158,7 @@ public function testNewFolder() {
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->expects($this->any())
 			->method('getUser')
@@ -182,7 +182,7 @@ public function testNewFolderDeepParent() {
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->expects($this->any())
 			->method('getUser')
@@ -209,7 +209,7 @@ public function testNewFolderNotPermitted() {
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->method('getUser')
 			->willReturn($this->user);
@@ -226,7 +226,7 @@ public function testNewFile() {
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->expects($this->any())
 			->method('getUser')
@@ -253,7 +253,7 @@ public function testNewFileNotPermitted() {
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->method('getUser')
 			->willReturn($this->user);
@@ -270,7 +270,7 @@ public function testGetFreeSpace() {
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->method('getUser')
 			->willReturn($this->user);
@@ -287,7 +287,7 @@ public function testSearch() {
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->method('getUser')
 			->willReturn($this->user);
@@ -328,7 +328,7 @@ public function testSearchInRoot() {
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
 			->setMethods(['getUser', 'getMountsIn', 'getMount'])
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->expects($this->any())
 			->method('getUser')
@@ -368,7 +368,7 @@ public function testSearchInStorageRoot() {
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->method('getUser')
 			->willReturn($this->user);
@@ -408,7 +408,7 @@ public function testSearchSubStorages() {
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->expects($this->any())
 			->method('getUser')
@@ -478,7 +478,7 @@ public function testGetById() {
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
 			->setMethods(['getMountsIn', 'getMount'])
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$storage = $this->createMock(\OC\Files\Storage\Storage::class);
 		$mount = new MountPoint($storage, '/bar');
@@ -525,7 +525,7 @@ public function testGetByIdMountRoot() {
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
 			->setMethods(['getMountsIn', 'getMount'])
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$storage = $this->createMock(\OC\Files\Storage\Storage::class);
 		$mount = new MountPoint($storage, '/bar');
@@ -568,7 +568,7 @@ public function testGetByIdOutsideFolder() {
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
 			->setMethods(['getMountsIn', 'getMount'])
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$storage = $this->createMock(\OC\Files\Storage\Storage::class);
 		$mount = new MountPoint($storage, '/bar');
@@ -610,7 +610,7 @@ public function testGetByIdMultipleStorages() {
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
 			->setMethods(['getMountsIn', 'getMount'])
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$storage = $this->createMock(\OC\Files\Storage\Storage::class);
 		$mount1 = new MountPoint($storage, '/bar');
@@ -672,7 +672,7 @@ public function testGetUniqueName($name, $existingFiles, $expected) {
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
 			->setMethods(['getUser', 'getMountsIn', 'getMount'])
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 
 		$view->expects($this->any())
@@ -697,7 +697,7 @@ public function testRecent(): void {
 		/** @var \PHPUnit\Framework\MockObject\MockObject|\OC\Files\Node\Root $root */
 		$root = $this->getMockBuilder(Root::class)
 			->setMethods(['getUser', 'getMountsIn', 'getMount'])
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		/** @var \PHPUnit\Framework\MockObject\MockObject|\OC\Files\FileInfo $folderInfo */
 		$folderInfo = $this->getMockBuilder(FileInfo::class)
@@ -762,7 +762,7 @@ public function testRecentFolder() {
 		/** @var \PHPUnit\Framework\MockObject\MockObject|\OC\Files\Node\Root $root */
 		$root = $this->getMockBuilder(Root::class)
 			->setMethods(['getUser', 'getMountsIn', 'getMount'])
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		/** @var \PHPUnit\Framework\MockObject\MockObject|\OC\Files\FileInfo $folderInfo */
 		$folderInfo = $this->getMockBuilder(FileInfo::class)
@@ -826,7 +826,7 @@ public function testRecentJail() {
 		/** @var \PHPUnit\Framework\MockObject\MockObject|\OC\Files\Node\Root $root */
 		$root = $this->getMockBuilder(Root::class)
 			->setMethods(['getUser', 'getMountsIn', 'getMount'])
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		/** @var \PHPUnit\Framework\MockObject\MockObject|\OC\Files\FileInfo $folderInfo */
 		$folderInfo = $this->getMockBuilder(FileInfo::class)
@@ -910,7 +910,7 @@ public function testSearchSubStoragesLimitOffset(int $offset, int $limit, array
 		$manager = $this->createMock(Manager::class);
 		$view = $this->getRootViewMock();
 		$root = $this->getMockBuilder(Root::class)
-			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$manager, $view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 		$root->expects($this->any())
 			->method('getUser')
diff --git a/tests/lib/Files/Node/HookConnectorTest.php b/tests/lib/Files/Node/HookConnectorTest.php
index 0501c175a5f6a..80a94462bba2c 100644
--- a/tests/lib/Files/Node/HookConnectorTest.php
+++ b/tests/lib/Files/Node/HookConnectorTest.php
@@ -13,6 +13,7 @@
 use OC\Files\Node\Root;
 use OC\Files\Storage\Temporary;
 use OC\Files\View;
+use OC\Memcache\ArrayCache;
 use OCP\EventDispatcher\GenericEvent as APIGenericEvent;
 use OCP\EventDispatcher\IEventDispatcher;
 use OCP\Files\Events\Node\AbstractNodeEvent;
@@ -30,6 +31,7 @@
 use OCP\Files\Events\Node\NodeTouchedEvent;
 use OCP\Files\Events\Node\NodeWrittenEvent;
 use OCP\Files\Node;
+use OCP\ICacheFactory;
 use OCP\IUserManager;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\EventDispatcher\GenericEvent;
@@ -67,6 +69,11 @@ protected function setUp(): void {
 		// this will setup the FS
 		$this->loginAsUser($this->userId);
 		$this->registerMount($this->userId, new Temporary(), '/' . $this->userId . '/files/');
+		$cacheFactory = $this->createMock(ICacheFactory::class);
+		$cacheFactory->method('createLocal')
+			->willReturnCallback(function () {
+				return new ArrayCache();
+			});
 		$this->view = new View();
 		$this->root = new Root(
 			Filesystem::getMountManager(),
@@ -75,7 +82,8 @@ protected function setUp(): void {
 			\OC::$server->getUserMountCache(),
 			$this->createMock(LoggerInterface::class),
 			$this->createMock(IUserManager::class),
-			$this->createMock(IEventDispatcher::class)
+			$this->createMock(IEventDispatcher::class),
+			$cacheFactory,
 		);
 		$this->eventDispatcher = \OC::$server->query(IEventDispatcher::class);
 	}
diff --git a/tests/lib/Files/Node/IntegrationTest.php b/tests/lib/Files/Node/IntegrationTest.php
index 5ef5a134e1bb2..dacc65ad4d120 100644
--- a/tests/lib/Files/Node/IntegrationTest.php
+++ b/tests/lib/Files/Node/IntegrationTest.php
@@ -11,8 +11,10 @@
 use OC\Files\Node\Root;
 use OC\Files\Storage\Temporary;
 use OC\Files\View;
+use OC\Memcache\ArrayCache;
 use OCP\EventDispatcher\IEventDispatcher;
 use OCP\Files\Mount\IMountManager;
+use OCP\ICacheFactory;
 use OCP\IUserManager;
 use Psr\Log\LoggerInterface;
 use Test\Traits\UserTrait;
@@ -52,6 +54,11 @@ protected function setUp(): void {
 
 		$user = $this->createUser($this->getUniqueID('user'), '');
 		$this->loginAsUser($user->getUID());
+		$cacheFactory = $this->createMock(ICacheFactory::class);
+		$cacheFactory->method('createLocal')
+			->willReturnCallback(function () {
+				return new ArrayCache();
+			});
 
 		$this->view = new View();
 		$this->root = new Root(
@@ -61,7 +68,8 @@ protected function setUp(): void {
 			\OC::$server->getUserMountCache(),
 			$this->createMock(LoggerInterface::class),
 			$this->createMock(IUserManager::class),
-			$this->createMock(IEventDispatcher::class)
+			$this->createMock(IEventDispatcher::class),
+			$cacheFactory,
 		);
 		$storage = new Temporary([]);
 		$subStorage = new Temporary([]);
diff --git a/tests/lib/Files/Node/NodeTest.php b/tests/lib/Files/Node/NodeTest.php
index 4f782f28a36e9..85a7f8a82f3c4 100644
--- a/tests/lib/Files/Node/NodeTest.php
+++ b/tests/lib/Files/Node/NodeTest.php
@@ -11,12 +11,14 @@
 use OC\Files\FileInfo;
 use OC\Files\Mount\Manager;
 use OC\Files\View;
+use OC\Memcache\ArrayCache;
 use OCP\EventDispatcher\IEventDispatcher;
 use OCP\Files\IRootFolder;
 use OCP\Files\Mount\IMountPoint;
 use OCP\Files\Node;
 use OCP\Files\NotFoundException;
 use OCP\Files\Storage;
+use OCP\ICacheFactory;
 use OCP\IUser;
 use OCP\IUserManager;
 use Psr\Log\LoggerInterface;
@@ -43,6 +45,8 @@ abstract class NodeTest extends \Test\TestCase {
 	protected $userManager;
 	/** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */
 	protected $eventDispatcher;
+	/** @var ICacheFactory|\PHPUnit\Framework\MockObject\MockObject */
+	protected $cacheFactory;
 
 	protected function setUp(): void {
 		parent::setUp();
@@ -63,8 +67,13 @@ protected function setUp(): void {
 		$this->logger = $this->createMock(LoggerInterface::class);
 		$this->userManager = $this->createMock(IUserManager::class);
 		$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+		$this->cacheFactory = $this->createMock(ICacheFactory::class);
+		$this->cacheFactory->method('createLocal')
+			->willReturnCallback(function () {
+				return new ArrayCache();
+			});
 		$this->root = $this->getMockBuilder('\OC\Files\Node\Root')
-			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->eventDispatcher])
+			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->getMock();
 	}
 
@@ -174,7 +183,8 @@ public function testDeleteHooks() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 
 		$root->listen('\OC\Files', 'preDelete', $preListener);
@@ -422,7 +432,8 @@ public function testTouchHooks() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 		$root->listen('\OC\Files', 'preTouch', $preListener);
 		$root->listen('\OC\Files', 'postTouch', $postListener);
@@ -599,7 +610,7 @@ public function moveOrCopyProvider() {
 	public function testMoveCopyHooks($operationMethod, $viewMethod, $preHookName, $postHookName) {
 		/** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject $root */
 		$root = $this->getMockBuilder('\OC\Files\Node\Root')
-			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher])
+			->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
 			->setMethods(['get'])
 			->getMock();
 
diff --git a/tests/lib/Files/Node/RootTest.php b/tests/lib/Files/Node/RootTest.php
index e24283f0931ac..d9abe2955b11a 100644
--- a/tests/lib/Files/Node/RootTest.php
+++ b/tests/lib/Files/Node/RootTest.php
@@ -12,8 +12,10 @@
 use OC\Files\Mount\Manager;
 use OC\Files\Node\Folder;
 use OC\Files\View;
+use OC\Memcache\ArrayCache;
 use OCP\Cache\CappedMemoryCache;
 use OCP\EventDispatcher\IEventDispatcher;
+use OCP\ICacheFactory;
 use OCP\IUser;
 use OCP\IUserManager;
 use Psr\Log\LoggerInterface;
@@ -36,6 +38,8 @@ class RootTest extends \Test\TestCase {
 	private $userManager;
 	/** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */
 	private $eventDispatcher;
+	/** @var ICacheFactory|\PHPUnit\Framework\MockObject\MockObject */
+	protected $cacheFactory;
 
 	protected function setUp(): void {
 		parent::setUp();
@@ -50,6 +54,11 @@ protected function setUp(): void {
 		$this->logger = $this->createMock(LoggerInterface::class);
 		$this->userManager = $this->createMock(IUserManager::class);
 		$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+		$this->cacheFactory = $this->createMock(ICacheFactory::class);
+		$this->cacheFactory->method('createLocal')
+			->willReturnCallback(function () {
+				return new ArrayCache();
+			});
 	}
 
 	/**
@@ -82,7 +91,8 @@ public function testGet() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 
 		$view->expects($this->once())
@@ -114,7 +124,8 @@ public function testGetNotFound() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 
 		$view->expects($this->once())
@@ -138,7 +149,8 @@ public function testGetInvalidPath() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 
 		$root->get('/../foo');
@@ -156,7 +168,8 @@ public function testGetNoStorages() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 
 		$root->get('/bar/foo');
@@ -170,7 +183,8 @@ public function testGetUserFolder() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 		$user = $this->createMock(IUser::class);
 		$user
@@ -211,7 +225,8 @@ public function testGetUserFolderWithNoUserObj() {
 			$this->userMountCache,
 			$this->logger,
 			$this->userManager,
-			$this->eventDispatcher
+			$this->eventDispatcher,
+			$this->cacheFactory,
 		);
 		$this->userManager
 			->expects($this->once())
