Skip to content

Commit a1d3ed8

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

File tree

6 files changed

+447
-6
lines changed

6 files changed

+447
-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: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
use OC\Hooks\Emitter;
1111
use OC\Hooks\EmitterTrait;
1212
use OCP\Diagnostics\IEventLogger;
13+
use OCP\Files\Cache\ICacheEntry;
14+
use OCP\Files\Config\ICachedMountInfo;
1315
use OCP\Files\Config\IHomeMountProvider;
1416
use OCP\Files\Config\IMountProvider;
1517
use OCP\Files\Config\IMountProviderCollection;
18+
use OCP\Files\Config\IPartialMountProvider;
1619
use OCP\Files\Config\IRootMountProvider;
1720
use OCP\Files\Config\IUserMountCache;
1821
use OCP\Files\Mount\IMountManager;
@@ -78,18 +81,47 @@ public function getMountsForUser(IUser $user): array {
7881
return $this->getUserMountsForProviders($user, $this->providers);
7982
}
8083

84+
/**
85+
* @param ICacheEntry[] $mountsMetadata
86+
* @param ICachedMountInfo[] $mountsInfo
87+
* @return IMountPoint[]
88+
*/
89+
public function getUserMountsFromProviderByPath(
90+
string $providerClass,
91+
string $path,
92+
array $mountsInfo,
93+
array $mountsMetadata,
94+
): array {
95+
$provider = $this->providers[$providerClass] ?? null;
96+
if ($provider === null) {
97+
return [];
98+
}
99+
100+
if (!is_a($providerClass, IPartialMountProvider::class, true)) {
101+
throw new \LogicException(
102+
'Mount provider does not support partial mounts'
103+
);
104+
}
105+
106+
return $provider->getMountsFromMountPoints(
107+
$path,
108+
$mountsInfo,
109+
$mountsMetadata,
110+
$this->loader,
111+
);
112+
}
113+
81114
/**
82115
* Convenience method that returns mounts coming from the specified provider classes
83116
* and if registered in the current collection instance.
84117
*
118+
* @inheritdoc
119+
*
85120
* @return list<IMountPoint>
86121
*/
87122
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);
123+
$providers = $this->getProvidersByClass($this->providers, $mountProviderClasses);
124+
return $this->getUserMountsForProviders($user, array_values($providers));
93125
}
94126

95127
/**
@@ -245,4 +277,12 @@ public function getHomeProviders(): array {
245277
public function getRootProviders(): array {
246278
return $this->rootProviders;
247279
}
280+
281+
/**
282+
* TODO: replace with a proxy function in this class, probably getting the
283+
* loader out of here is not a good idea
284+
*/
285+
public function getLoader(): IStorageFactory {
286+
return $this->loader;
287+
}
248288
}

lib/private/Files/Config/UserMountCache.php

Lines changed: 214 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,104 @@ 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+
// todo: those are the only possible valid mountpoints because they
257+
// end with /
258+
$subPaths = $this->splitInSubPaths($path);
259+
$cachedMountInfos = $this->getCachedMountsByPaths($userId, $subPaths);
260+
$missingMountPaths = array_diff($subPaths, array_keys($cachedMountInfos));
261+
262+
if (empty($missingMountPaths)) {
263+
/** @var string[] $cachedMountPoints */
264+
$cachedMountPoints = array_filter(
265+
array_map(
266+
fn (string $path,
267+
) => $cachedMountInfos[$path]?->getMountPoint(),
268+
$subPaths
269+
)
270+
);
271+
272+
// sort by length desc
273+
usort($cachedMountPoints, fn (string $a, string $b) => strlen($b) - strlen($a));
274+
275+
$longestPath = array_shift($cachedMountPoints);
276+
return $cachedMountInfos[$longestPath];
277+
}
278+
279+
$builder = $this->connection->getQueryBuilder();
280+
$query = $builder->select(
281+
'storage_id',
282+
'root_id',
283+
'user_id',
284+
'mount_point',
285+
'mount_id',
286+
'mount_provider_class'
287+
)->from('mounts', 'm')
288+
->where(
289+
$builder->expr()->eq(
290+
'user_id',
291+
$builder->createNamedParameter($userId
292+
),
293+
)
294+
)->andWhere(
295+
$builder->expr()->in(
296+
'mount_point',
297+
$builder->createNamedParameter(
298+
$missingMountPaths,
299+
IQueryBuilder::PARAM_STR_ARRAY
300+
)
301+
)
302+
)->orderBy($builder->func()->charLength('mount_point'), 'DESC');
303+
304+
$result = $query->executeQuery();
305+
if ($result->rowCount() === 0) {
306+
$result->closeCursor();
307+
// todo: BUG, this still needs to return the longest matching path!
308+
// sloppy fix for development
309+
// mark the leftover paths as not found
310+
$usersMountsByPath = &$this->usersMountsByPath[$userId];
311+
foreach ($subPaths as $subPath) {
312+
if (!array_key_exists($subPath, $usersMountsByPath)) {
313+
$usersMountsByPath[$subPath] = null;
314+
}
315+
}
316+
// call the function again, as it will yield the longest path now
317+
return $this->getLongestMatchingMount($userId, $path);
318+
}
319+
320+
$this->usersMountsByPath[$userId] ??= [];
321+
$usersMountsByPath = &$this->usersMountsByPath[$userId];
322+
$firstMount = null;
323+
while ($mountPointRow = $result->fetch()) {
324+
$mount = $this->dbRowToMountInfo(
325+
$mountPointRow,
326+
[$this, 'getInternalPathForMountInfo']
327+
);
328+
$firstMount ??= $mount;
329+
$usersMountsByPath[$mount->getMountPoint()] = $mount;
330+
}
331+
$result->closeCursor();
332+
333+
// cache the info that the sub paths have no mount-point associated
334+
foreach ($subPaths as $subPath) {
335+
if (!array_key_exists($subPath, $usersMountsByPath)) {
336+
$usersMountsByPath[$subPath] = null;
337+
}
338+
}
339+
340+
return $firstMount;
341+
}
342+
239343
/**
240344
* @param IUser $user
241345
* @return ICachedMountInfo[]
@@ -485,6 +589,116 @@ public function clear(): void {
485589
$this->mountsForUsers = new CappedMemoryCache();
486590
}
487591

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

0 commit comments

Comments
 (0)