Skip to content

feat(mount-provider): implement IPartialMountProvider#2378

Open
cristianscheid wants to merge 2 commits intomasterfrom
feat/2308/partial-mount-provider
Open

feat(mount-provider): implement IPartialMountProvider#2378
cristianscheid wants to merge 2 commits intomasterfrom
feat/2308/partial-mount-provider

Conversation

@cristianscheid
Copy link
Contributor

@cristianscheid cristianscheid commented Feb 27, 2026

Summary

Implemented IPartialMountProvider in CircleMountProvider by adding getMountsForPath() and added limitToMountpoint() to CoreQueryBuilder to support filtering.

Testing

To test this implementation, I did the following:

  • Temporarily commented out the if (!$this->configService->isLocalInstance($event->getOrigin())) condition in lib/FederatedItems/Files/FileShare::manage() to allow mount entries to be created locally
  • Created teams mock_team_1, mock_team_2 and mock_team_3
  • Created folders mock_folder_1, mock_folder_2 and mock_folder_3 and shared with their respective folders (mock_team_1, mock_team_2 and mock_team_3)
  • Temporarily commented out the early return in lib/AppInfo/Application::registerMountProvider() to force registration of CircleMountProvider regardless of GlobalScale availability
  • Using Xdebug with a breakpoint on getMountsForUser(), accessed nextcloud.local/index.php/apps/dashboard/, triggering the getMountsForUser() method
  • Instead of running the regular method flow, I created some mock params and called getMountsForPath() directly:
$makeMountArg = function(string $path) use ($user): \OCP\Files\Config\MountProviderArgs {
	$mountInfo = new class($user, $path) implements \OCP\Files\Config\ICachedMountInfo {
		public function __construct(private \OCP\IUser $user, private string $path) {}
		public function getUser(): \OCP\IUser { return $this->user; }
		public function getMountPoint(): string { return '/' . $this->user->getUID() . '/files/' . $this->path; }
		public function getStorageId(): int { return 0; }
		public function getRootId(): int { return 0; }
		public function getMountPointNode(): ?\OCP\Files\Node { return null; }
		public function getMountId(): ?int { return null; }
		public function getRootInternalPath(): string { return ''; }
		public function getMountProvider(): string { return ''; }
		public function getKey(): string { return ''; }
	};
	return new \OCP\Files\Config\MountProviderArgs(
		$mountInfo,
		new \OC\Files\Cache\CacheEntry([])
	);
};

$this->getMountsForPath(
	'/' . $user->getUID() . '/files/mock_folder',
	false,
	[
		$makeMountArg('mock_folder_1'),
		$makeMountArg('mock_folder_2'),
		$makeMountArg('another_folder'),
	],
	$loader
);

Results

  • Path filtering worked correctly when forChildren was set to false. With multiple folders on my setup (mock_team_1, mock_team_2 and mock_team_3), in the test above only the requested mounts that exist in the DB were returned for the given paths when calling getForUser() inside getMountsForPath(), confirming that limitToMountpoints() is filtering correctly at the database level.
  • forChildren did not work as expected when set to true. During testing, circles always stored mountpoints relatively. For example, sharing a folder created inside mock_folder generated a circles_mount entry with mountpoint /mock_subfolder instead of /mock_folder/mock_subfolder. As a result, the LIKE '/mock_folder/%' filter used when forChildren is true will not match these entries.
  • During testing, generateCircleMount() threw an exception in both the original getMountsForUser() and on new method getMountsForPath(). I believe this happened because the mount entries were artificially created by bypassing the remote instance check in FileShare::manage(), resulting in incomplete data compared to what would be present in a real GlobalScale environment.

Checklist

AI (if applicable)

  • The content of this PR was partly or fully generated using AI

Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
…single query

Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
@cristianscheid
Copy link
Contributor Author

@provokateurin I've addressed your review comments and updated the PR description with new testing instructions. Thanks!


if ($forChildren) {
foreach ($paths as $path) {
$orX->add($expr->like($aliasMount . '.mountpoint', $this->createNamedParameter($path . '/%')));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$orX->add($expr->like($aliasMount . '.mountpoint', $this->createNamedParameter($path . '/%')));
$orX->add($expr->like($aliasMount . '.mountpoint', $this->createNamedParameter($path . '/_%')));

This is what we settled on with @salmart-dev and @artonge, because a mountpoint could have a trailing slash, but not children, and would match without this.

}

$expr = $this->expr();
$orX = $expr->orX();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move this into the $forChildren clause, since it's only used there.

Comment on lines +1578 to +1580
* @param string $aliasMount
* @param string[] $paths
* @param bool $forChildren
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param string $aliasMount
* @param string[] $paths
* @param bool $forChildren
* @param list<string> $paths

Less noise and more precise.

Comment on lines 60 to +62
* @param IFederatedUser $federatedUser
* @param string[] $paths
* @param bool $forChildren
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param IFederatedUser $federatedUser
* @param string[] $paths
* @param bool $forChildren
* @param list<string> $paths

Comment on lines +270 to +276
if (!isset($userMountRequests[$user->getUID()])) {
$federatedUser = $this->federatedUserService->getLocalFederatedUser($user->getUID());
$userMountRequests[$user->getUID()] = [
'federatedUser' => $federatedUser,
'paths' => [],
];
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!isset($userMountRequests[$user->getUID()])) {
$federatedUser = $this->federatedUserService->getLocalFederatedUser($user->getUID());
$userMountRequests[$user->getUID()] = [
'federatedUser' => $federatedUser,
'paths' => [],
];
}
$userMountRequests[$user->getUID()] ??= [
'federatedUser' => $this->federatedUserService->getLocalFederatedUser($user->getUID()),
'paths' => [],
];

Might be easier to read 🤷‍♀️

];
}

$userMountRequests[$user->getUID()]['paths'][] = '/' . implode('/', array_slice($parts, 3));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you check that the leading slash is correct here?

$this->fixDuplicateFile($uid, $item);
$mounts[$mountPoint] = $this->generateCircleMount($item, $loader);
} catch (\Exception $e) {
$this->logger->warning('issue with teams\' partial mounts', ['exception' => $e]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$this->logger->warning('issue with teams\' partial mounts', ['exception' => $e]);
$this->logger->error('Failed to create Teams mount', ['exception' => $e]);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also rethrow or just continue? Maybe @salmart-dev you can answer that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Implement the new partial mount provider API

2 participants