@@ -31,6 +31,8 @@ class FolderStorageManager {
3131 private readonly bool $ enableEncryption ;
3232 /** @var array<string, Folder> */
3333 private array $ cachedFolders = [];
34+ /** @var array<int, IStorage> */
35+ private array $ cachedSeparateStorages = [];
3436
3537 public function __construct (
3638 private readonly IRootFolder $ rootFolder ,
@@ -97,16 +99,22 @@ private function getBaseStorageForFolderSeparate(
9799 bool $ init = false ,
98100 array $ options = [],
99101 ): IStorage {
100- if ($ this ->primaryObjectStoreConfig ->hasObjectStore ()) {
102+ // Cache the raw Local/ObjectStoreStorage instance per folder ID within a request.
103+ // The underlying storage is the same regardless of user or type — only the Jail root and ACL wrapper differ.
104+ if (isset ($ this ->cachedSeparateStorages [$ folderId ])) {
105+ $ storage = $ this ->cachedSeparateStorages [$ folderId ];
106+ } elseif ($ this ->primaryObjectStoreConfig ->hasObjectStore ()) {
101107 $ bucket = $ options ['bucket ' ] ?? null ;
102108 if ($ bucket !== null && !is_string ($ bucket )) {
103109 throw new Exception ('bucket is not a string. ' );
104110 }
105111 /** @var Storage $storage */
106112 $ storage = $ this ->getBaseStorageForFolderSeparateStorageObject ($ folderId , $ init , $ bucket );
113+ $ this ->cachedSeparateStorages [$ folderId ] = $ storage ;
107114 } else {
108115 /** @var Storage $storage */
109116 $ storage = $ this ->getBaseStorageForFolderSeparateStorageLocal ($ folderId , $ init );
117+ $ this ->cachedSeparateStorages [$ folderId ] = $ storage ;
110118 }
111119
112120 if ($ folder ?->acl && $ user ) {
@@ -116,7 +124,7 @@ private function getBaseStorageForFolderSeparate(
116124 'acl_manager ' => $ aclManager ,
117125 'in_share ' => $ inShare ,
118126 'folder_id ' => $ folderId ,
119- 'storage_id ' => $ storage ->getCache ()->getNumericStorageId (),
127+ 'storage_id ' => $ folder -> storageId ?: $ storage ->getCache ()->getNumericStorageId (),
120128 ]);
121129 }
122130
@@ -206,18 +214,75 @@ private function getBaseStorageForFolderRootJail(
206214 bool $ inShare = false ,
207215 string $ type = 'files ' ,
208216 ): IStorage {
209- if (isset ($ this ->cachedFolders ['root ' ])) {
210- $ parentFolder = $ this ->cachedFolders ['root ' ];
211- } else {
212- try {
213- /** @var Folder $parentFolder */
214- $ parentFolder = $ this ->rootFolder ->get ('__groupfolders ' );
215- } catch (NotFoundException ) {
216- $ parentFolder = $ this ->rootFolder ->newFolder ('__groupfolders ' );
217- }
218- $ this ->cachedFolders ['root ' ] = $ parentFolder ;
217+ // Fast path: for type='files' with folder metadata available (the PROPFIND hot path).
218+ // Constructs the Jail storage directly from already-known data without any per-folder DB queries.
219+ if ($ type === 'files ' && $ folder !== null ) {
220+ return $ this ->getBaseStorageForFolderRootJailFast ($ folderId , $ folder , $ user , $ inShare );
219221 }
220222
223+ // Slow path: for trash, versions, delete operations, and fallback cases.
224+ return $ this ->getBaseStorageForFolderRootJailSlow ($ folderId , $ folder , $ user , $ inShare , $ type );
225+ }
226+
227+ /**
228+ * Fast path for type='files' with folder metadata available.
229+ * Constructs the Jail storage directly from already-known data without any per-folder DB queries:
230+ * - Gets the root storage from the cached __groupfolders parent (resolved once via getRootGroupFolder())
231+ * - Computes the Jail root path deterministically instead of calling parentFolder->get(folderId)
232+ * - Uses $folder->storageId for the ACL wrapper instead of querying the cache
233+ */
234+ private function getBaseStorageForFolderRootJailFast (
235+ int $ folderId ,
236+ FolderDefinition $ folder ,
237+ ?IUser $ user = null ,
238+ bool $ inShare = false ,
239+ ): IStorage {
240+ $ parentFolder = $ this ->getRootGroupFolder ();
241+
242+ /** @var Storage $rootStorage */
243+ $ rootStorage = $ parentFolder ->getStorage ();
244+ $ rootPath = $ parentFolder ->getInternalPath () . '/ ' . $ folderId ;
245+
246+ // apply acl before jail
247+ if ($ folder ->acl && $ user ) {
248+ $ aclManager = $ this ->aclManagerFactory ->getACLManager ($ user );
249+ $ rootStorage = new ACLStorageWrapper ([
250+ 'storage ' => $ rootStorage ,
251+ 'acl_manager ' => $ aclManager ,
252+ 'in_share ' => $ inShare ,
253+ 'folder_id ' => $ folderId ,
254+ 'storage_id ' => $ folder ->storageId ,
255+ ]);
256+ }
257+
258+ if ($ this ->enableEncryption ) {
259+ return new GroupFolderEncryptionJail ([
260+ 'storage ' => $ rootStorage ,
261+ 'root ' => $ rootPath ,
262+ ]);
263+ }
264+
265+ return new Jail ([
266+ 'storage ' => $ rootStorage ,
267+ 'root ' => $ rootPath ,
268+ ]);
269+ }
270+
271+ /**
272+ * Slow path: resolves nodes via the filesystem for trash, versions, delete operations, and fallback cases.
273+ * These are not in the PROPFIND hot path.
274+ *
275+ * @param 'files'|'trash'|'versions'|'' $type
276+ */
277+ private function getBaseStorageForFolderRootJailSlow (
278+ int $ folderId ,
279+ ?FolderDefinition $ folder = null ,
280+ ?IUser $ user = null ,
281+ bool $ inShare = false ,
282+ string $ type = 'files ' ,
283+ ): IStorage {
284+ $ parentFolder = $ this ->getRootGroupFolder ();
285+
221286 if ($ type !== 'files ' ) {
222287 if (isset ($ this ->cachedFolders [$ type ])) {
223288 $ parentFolder = $ this ->cachedFolders [$ type ];
@@ -250,7 +315,7 @@ private function getBaseStorageForFolderRootJail(
250315 'acl_manager ' => $ aclManager ,
251316 'in_share ' => $ inShare ,
252317 'folder_id ' => $ folderId ,
253- 'storage_id ' => $ rootStorage ->getCache ()->getNumericStorageId (),
318+ 'storage_id ' => $ folder -> storageId ?: $ rootStorage ->getCache ()->getNumericStorageId (),
254319 ]);
255320 }
256321
@@ -267,6 +332,26 @@ private function getBaseStorageForFolderRootJail(
267332 ]);
268333 }
269334
335+ /**
336+ * Get the __groupfolders root folder, cached for the duration of the request.
337+ * Shared by both fast and slow paths.
338+ */
339+ private function getRootGroupFolder (): Folder {
340+ if (isset ($ this ->cachedFolders ['root ' ])) {
341+ return $ this ->cachedFolders ['root ' ];
342+ }
343+
344+ try {
345+ /** @var Folder $parentFolder */
346+ $ parentFolder = $ this ->rootFolder ->get ('__groupfolders ' );
347+ } catch (NotFoundException ) {
348+ $ parentFolder = $ this ->rootFolder ->newFolder ('__groupfolders ' );
349+ }
350+ $ this ->cachedFolders ['root ' ] = $ parentFolder ;
351+
352+ return $ parentFolder ;
353+ }
354+
270355 public function deleteStoragesForFolder (FolderDefinition $ folder ): void {
271356 foreach (['files ' , 'trash ' , 'versions ' , '' ] as $ type ) {
272357 $ storage = $ this ->getBaseStorageForFolder ($ folder ->id , $ folder ->useSeparateStorage (), $ folder , null , false , $ type );
0 commit comments