@@ -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