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