Skip to content

Commit ae59c16

Browse files
committed
fix(api): fix performance issue on PROPFIND api
This fixes #4095
1 parent 4fe33f6 commit ae59c16

1 file changed

Lines changed: 98 additions & 13 deletions

File tree

lib/Mount/FolderStorageManager.php

Lines changed: 98 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)