Skip to content

Commit 7f95870

Browse files
committed
WIP: add a second path for setupForPath for authoritative setup
[skip ci] Signed-off-by: Salvatore Martire <[email protected]>
1 parent a304126 commit 7f95870

File tree

6 files changed

+400
-6
lines changed

6 files changed

+400
-6
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OC\Files\Cache;
6+
7+
use OCP\Cache\CappedMemoryCache;
8+
use OCP\Files\Cache\ICacheEntry;
9+
10+
/**
11+
* This class uses FileAccess to fetch data to populate ICacheEntry objects
12+
* and caches them in memory for subsequent access.
13+
*/
14+
class FileMetadataCache {
15+
16+
private CappedMemoryCache $fileCache;
17+
18+
public function __construct(
19+
private readonly FileAccess $fileAccess,
20+
) {
21+
$this->fileCache = new CappedMemoryCache();
22+
}
23+
24+
/**
25+
* Returns file metadata by retrieving it from an in-memory cache or the
26+
* database.
27+
*
28+
* @param int[] $fileIds
29+
* @return array<int, ICacheEntry|null>
30+
* @see FileAccess::getByFileIds()
31+
*/
32+
public function getByFileIds(array $fileIds): array {
33+
$cacheArray = $this->fileCache->getData();
34+
// question: why doesn't CappedMemoryCache::hasKey use // array_key_exists?
35+
$arrayKeyExists = fn (int $id): bool => !array_key_exists(
36+
$id,
37+
$cacheArray
38+
);
39+
$missingIds = array_filter($fileIds, $arrayKeyExists(...));
40+
41+
if (!empty($missingIds)) {
42+
$missingMetadata = $this->fileAccess->getByFileIds($missingIds);
43+
foreach ($missingMetadata as $id => $metadata) {
44+
$this->fileCache->set((string)$id, $metadata);
45+
}
46+
}
47+
48+
return array_reduce(
49+
$fileIds,
50+
function (array $carry, int $id) {
51+
$carry[$id] = $this->fileCache->get((string)$id);
52+
return $carry;
53+
}, []);
54+
}
55+
}

lib/private/Files/Config/MountProviderCollection.php

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,34 @@ public function getMountsForUser(IUser $user): array {
7878
return $this->getUserMountsForProviders($user, $this->providers);
7979
}
8080

81+
/**
82+
* Convenience function to get registered IMountProvider instances by their class name.
83+
*
84+
* @param array<IMountProvider> $providers
85+
* @param array<class-string<IMountProvider>> $mountProviderClasses
86+
* @return array<int, IMountProvider> IMountProvider keyed-by the respective class
87+
*/
88+
public function getProvidersByClass(array $providers, array $mountProviderClasses): array {
89+
return array_filter(
90+
$providers,
91+
fn (IMountProvider $mountProvider) => (in_array(
92+
get_class($mountProvider),
93+
$mountProviderClasses
94+
))
95+
);
96+
}
97+
8198
/**
8299
* Convenience method that returns mounts coming from the specified provider classes
83100
* and if registered in the current collection instance.
84101
*
102+
* @inheritdoc
103+
*
85104
* @return list<IMountPoint>
86105
*/
87106
public function getUserMountsForProviderClasses(IUser $user, array $mountProviderClasses): array {
88-
$providers = array_filter(
89-
$this->providers,
90-
fn (IMountProvider $mountProvider) => (in_array(get_class($mountProvider), $mountProviderClasses))
91-
);
92-
return $this->getUserMountsForProviders($user, $providers);
107+
$providers = $this->getProvidersByClass($this->providers, $mountProviderClasses);
108+
return $this->getUserMountsForProviders($user, array_values($providers));
93109
}
94110

95111
/**

lib/private/Files/Config/UserMountCache.php

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ class UserMountCache implements IUserMountCache {
4141
private CappedMemoryCache $internalPathCache;
4242
/** @var CappedMemoryCache<array> */
4343
private CappedMemoryCache $cacheInfoCache;
44+
/** @var CappedMemoryCache<array<string, ICachedMountInfo|null>|null> */
45+
private CappedMemoryCache $usersMountsByPath;
46+
/** @var CappedMemoryCache<array<string, ICachedMountInfo|null>|null> */
47+
private CappedMemoryCache $childMounts;
4448

4549
/**
4650
* UserMountCache constructor.
@@ -55,6 +59,8 @@ public function __construct(
5559
$this->cacheInfoCache = new CappedMemoryCache();
5660
$this->internalPathCache = new CappedMemoryCache();
5761
$this->mountsForUsers = new CappedMemoryCache();
62+
$this->usersMountsByPath = new CappedMemoryCache();
63+
$this->childMounts = new CappedMemoryCache();
5864
}
5965

6066
public function registerMounts(IUser $user, array $mounts, ?array $mountProviderClasses = null) {
@@ -236,6 +242,93 @@ private function dbRowToMountInfo(array $row, ?callable $pathCallback = null): I
236242
}
237243
}
238244

245+
246+
/**
247+
* Given a userId and a path, returns the mount information of the best
248+
* matching path belonging to the user with the specified userId.
249+
*
250+
* @param string $userId
251+
* @param string $path
252+
* @return ICachedMountInfo|null
253+
*/
254+
private function getLongestMatchingMount(string $userId, string $path):
255+
?ICachedMountInfo {
256+
$subPaths = $this->splitInSubPaths($path);
257+
$cachedMountInfos = $this->getCachedMountsByPaths($userId, $subPaths);
258+
$missingMountPaths = array_diff($subPaths, array_keys($cachedMountInfos));
259+
260+
if (empty($missingMountPaths)) {
261+
/** @var string[] $cachedMountPoints */
262+
$cachedMountPoints = array_filter(
263+
array_map(
264+
fn (string $path,
265+
) => $cachedMountInfos[$path]?->getMountPoint(),
266+
$subPaths
267+
)
268+
);
269+
270+
// sort by length desc
271+
usort($cachedMountPoints, fn (string $a, string $b) => strlen($b) - strlen($a));
272+
273+
$longestPath = array_shift($cachedMountPoints);
274+
return $cachedMountInfos[$longestPath];
275+
}
276+
277+
$builder = $this->connection->getQueryBuilder();
278+
$query = $builder->select(
279+
'storage_id',
280+
'root_id',
281+
'user_id',
282+
'mount_point',
283+
'mount_id',
284+
'mount_provider_class'
285+
)->from('mounts', 'm')
286+
->where(
287+
$builder->expr()->eq(
288+
'user_id',
289+
$builder->createNamedParameter($userId
290+
),
291+
)
292+
)->andWhere(
293+
$builder->expr()->in(
294+
'mount_point',
295+
$builder->createNamedParameter(
296+
$missingMountPaths,
297+
IQueryBuilder::PARAM_STR_ARRAY
298+
)
299+
)
300+
)->orderBy($builder->func()->charLength('mount_point'), 'DESC');
301+
302+
$result = $query->executeQuery();
303+
$mountPointRows = $result->fetchAll();
304+
$result->closeCursor();
305+
306+
if (empty($mountPointRows)) {
307+
return null;
308+
}
309+
310+
$this->usersMountsByPath[$userId] ??= [];
311+
$usersMountsByPath = &$this->usersMountsByPath[$userId];
312+
$firstMount = null;
313+
foreach ($mountPointRows as $mountPointRow) {
314+
$mount = $this->dbRowToMountInfo(
315+
$mountPointRow,
316+
[$this, 'getInternalPathForMountInfo']
317+
);
318+
$firstMount ??= $mount;
319+
$usersMountsByPath[$mount->getMountPoint()] = $mount;
320+
}
321+
322+
// cache the info that the split paths have no mount-point associated
323+
foreach ($subPaths as $path) {
324+
if (!array_key_exists($path, $usersMountsByPath)) {
325+
$usersMountsByPath[$path] = null;
326+
}
327+
}
328+
329+
return $firstMount;
330+
}
331+
239332
/**
240333
* @param IUser $user
241334
* @return ICachedMountInfo[]
@@ -485,6 +578,116 @@ public function clear(): void {
485578
$this->mountsForUsers = new CappedMemoryCache();
486579
}
487580

581+
/**
582+
* Splits $path in an array of subpaths with a trailing '/'.
583+
*
584+
* @param string $path
585+
* @return array
586+
*/
587+
private function splitInSubPaths(string $path): array {
588+
$subPaths = [];
589+
$current = $path;
590+
while (true) {
591+
// paths always have a trailing slash in mount-points stored in the
592+
// oc_mounts table
593+
$subPaths[] = rtrim($current, '/') . '/';
594+
$current = dirname($current);
595+
if ($current === '/' || $current === '.') {
596+
break;
597+
}
598+
599+
}
600+
return $subPaths;
601+
}
602+
603+
/**
604+
* Returns an array of ICachedMountInfo, keyed by path.
605+
*
606+
* Note that null values are also possible, signalling that the path is not
607+
* associated with a mount-point.
608+
*
609+
* @param string[] $subPaths
610+
* @return array<string, ICachedMountInfo|null>
611+
*/
612+
public function getCachedMountsByPaths(string $userId, array $subPaths):
613+
array {
614+
$cachedUserPaths = $this->usersMountsByPath[$userId] ?? [];
615+
616+
return array_reduce(
617+
$subPaths,
618+
function ($carry, $path) use ($cachedUserPaths) {
619+
$hasCache = array_key_exists($path, $cachedUserPaths);
620+
if ($hasCache) {
621+
$carry[$path] = $cachedUserPaths[$path];
622+
}
623+
return $carry;
624+
},
625+
[]
626+
);
627+
}
628+
629+
/**
630+
* @inheritdoc
631+
*/
632+
public function getMountsForPath(IUser $user, string $path, bool $includeChildMounts): array {
633+
$mount = $this->getLongestMatchingMount($user->getUID(), $path);
634+
635+
if ($mount === null) {
636+
throw new NotFoundException('No mount for path ' . $path);
637+
}
638+
639+
$mounts = [$mount->getMountPoint() => $mount];
640+
641+
if ($includeChildMounts) {
642+
$mounts = array_merge($mounts, $this->getChildMounts($path));
643+
}
644+
645+
return $mounts;
646+
}
647+
648+
/**
649+
* Gets the child-mounts for the provided path.
650+
*
651+
* @param string $path
652+
* @return array
653+
* @throws \OCP\DB\Exception
654+
*/
655+
private function getChildMounts(string $path): array {
656+
$path = rtrim($path, '/') . '/';
657+
$cachedMounts = $this->childMounts[$path];
658+
if ($cachedMounts !== null) {
659+
return $cachedMounts;
660+
}
661+
662+
// todo: add a column in the oc_mounts to fetch direct children of the
663+
// mount.
664+
665+
$builder = $this->connection->getQueryBuilder();
666+
$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'mount_provider_class')
667+
->from('mounts', 'm')
668+
->where($builder->expr()->like('mount_point',
669+
$builder->createNamedParameter($path . '%'))
670+
)->andWhere($builder->expr()->neq('mount_point',
671+
$builder->createNamedParameter($path)));
672+
// todo: do we need to add WHERE mount_point IS NOT NULL?
673+
674+
$result = $query->executeQuery();
675+
676+
$rows = $result->fetchAll();
677+
$result->closeCursor();
678+
679+
$childMounts = array_filter(
680+
array_map(
681+
[$this, 'dbRowToMountInfo'],
682+
$rows
683+
)
684+
);
685+
686+
$this->childMounts[$path] = $childMounts;
687+
688+
return $childMounts;
689+
}
690+
488691
public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
489692
$mounts = $this->getMountsForUser($user);
490693
$mountPoints = array_map(function (ICachedMountInfo $mount) {

0 commit comments

Comments
 (0)