diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index e4a04ed31..f7b1dd975 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -147,7 +147,7 @@ jobs: - name: Extract NC logs if: failure() && matrix.containers != 'component' - run: docker logs nextcloud-cypress-tests-${{ env.APP_NAME }} > nextcloud.log + run: docker logs nextcloud-cypress-tests_${{ env.APP_NAME }} > nextcloud.log - name: Upload NC logs uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/appinfo/info.xml b/appinfo/info.xml index d5f267353..b0de39072 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -14,7 +14,7 @@ Folders can be configured from *Team folders* in the admin settings. After a folder is created, the admin can give access to the folder to one or more teams, control their write/sharing permissions and assign a quota for the folder. As of Hub 10/Nextcloud 31, the admin needs to be a part of the team to be able to assign it a Teamfolder. ]]> - 20.0.0-dev.0 + 20.0.0-dev.1 agpl Robin Appelman GroupFolders diff --git a/lib/ACL/ACLCacheWrapper.php b/lib/ACL/ACLCacheWrapper.php index 2c94c5495..17df40bea 100644 --- a/lib/ACL/ACLCacheWrapper.php +++ b/lib/ACL/ACLCacheWrapper.php @@ -27,7 +27,7 @@ private function getACLPermissionsForPath(string $path, array $rules = []): int if ($rules) { $permissions = $this->aclManager->getPermissionsForPathFromRules($path, $rules); } else { - $permissions = $this->aclManager->getACLPermissionsForPath($path); + $permissions = $this->aclManager->getACLPermissionsForPath($this->getNumericStorageId(), $path); } // if there is no read permissions, than deny everything @@ -89,6 +89,6 @@ public function searchQuery(ISearchQuery $query): array { private function preloadEntries(array $entries): array { $paths = array_map(fn (ICacheEntry $entry): string => $entry->getPath(), $entries); - return $this->aclManager->getRelevantRulesForPath($paths, false); + return $this->aclManager->getRelevantRulesForPath($this->getNumericStorageId(), $paths, false); } } diff --git a/lib/ACL/ACLManager.php b/lib/ACL/ACLManager.php index 3e79bab4a..d636f4892 100644 --- a/lib/ACL/ACLManager.php +++ b/lib/ACL/ACLManager.php @@ -12,7 +12,6 @@ use OCA\GroupFolders\Trash\TrashManager; use OCP\Cache\CappedMemoryCache; use OCP\Constants; -use OCP\Files\IRootFolder; use OCP\IUser; use Psr\Log\LoggerInterface; use RuntimeException; @@ -26,32 +25,20 @@ public function __construct( private readonly IUserMappingManager $userMappingManager, private readonly LoggerInterface $logger, private readonly IUser $user, - private readonly \Closure $rootFolderProvider, - private ?int $rootStorageId = null, private readonly bool $inheritMergePerUser = false, ) { $this->ruleCache = new CappedMemoryCache(); } - private function getRootStorageId(): int { - if ($this->rootStorageId === null) { - $provider = $this->rootFolderProvider; - /** @var IRootFolder $rootFolder */ - $rootFolder = $provider(); - $this->rootStorageId = $rootFolder->getMountPoint()->getNumericStorageId() ?? -1; - } - - return $this->rootStorageId; - } - /** * Get the list of rules applicable for a set of paths * + * @param int $storageId * @param string[] $paths * @param bool $cache whether to cache the retrieved rules * @return array sorted parent first */ - private function getRules(array $paths, bool $cache = true): array { + private function getRules(int $storageId, array $paths, bool $cache = true): array { // beware: adding new rules to the cache besides the cap // might discard former cached entries, so we can't assume they'll stay // cached, so we read everything out initially to be able to return it @@ -60,7 +47,7 @@ private function getRules(array $paths, bool $cache = true): array { $nonCachedPaths = array_filter($paths, fn (string $path): bool => !isset($rules[$path])); if (!empty($nonCachedPaths)) { - $newRules = $this->ruleManager->getRulesForFilesByPath($this->user, $this->getRootStorageId(), $nonCachedPaths); + $newRules = $this->ruleManager->getRulesForFilesByPath($this->user, $storageId, $nonCachedPaths); foreach ($newRules as $path => $rulesForPath) { if ($cache) { $this->ruleCache->set($path, $rulesForPath); @@ -75,6 +62,30 @@ private function getRules(array $paths, bool $cache = true): array { return $rules; } + /** + * Get the list of rules applicable for a set of paths + * + * @param int[] $fileIds + * @param bool $cache whether to cache the retrieved rules + * @return array sorted parent first + */ + public function getRulesByFileIds(array $fileIds, bool $cache = true): array { + $rules = []; + + $newRules = $this->ruleManager->getRulesForFilesByIds($this->user, $fileIds); + foreach ($newRules as $path => $rulesForPath) { + if ($cache) { + $this->ruleCache->set($path, $rulesForPath); + } + + $rules[$path] = $rulesForPath; + } + + ksort($rules); + + return $rules; + } + /** * Get a list of all path that might contain relevant rules when calculating the permissions for a path * @@ -130,22 +141,23 @@ private function getRelevantPaths(string $path): array { /** * Get the list of rules applicable for a set of paths, including rules for any parent * + * @param int $storageId * @param string[] $paths * @param bool $cache whether to cache the retrieved rules * @return array sorted parent first */ - public function getRelevantRulesForPath(array $paths, bool $cache = true): array { + public function getRelevantRulesForPath(int $storageId, array $paths, bool $cache = true): array { $allPaths = []; foreach ($paths as $path) { $allPaths = array_unique(array_merge($allPaths, $this->getRelevantPaths($path))); } - return $this->getRules($allPaths, $cache); + return $this->getRules($storageId, $allPaths, $cache); } - public function getACLPermissionsForPath(string $path): int { + public function getACLPermissionsForPath(int $storageId, string $path): int { $path = ltrim($path, '/'); - $rules = $this->getRules($this->getRelevantPaths($path)); + $rules = $this->getRules($storageId, $this->getRelevantPaths($path)); return $this->calculatePermissionsForPath($rules); } @@ -155,9 +167,9 @@ public function getACLPermissionsForPath(string $path): int { * * @param list $newRules */ - public function testACLPermissionsForPath(string $path, array $newRules): int { + public function testACLPermissionsForPath(int $storageId, string $path, array $newRules): int { $path = ltrim($path, '/'); - $rules = $this->getRules($this->getRelevantPaths($path)); + $rules = $this->getRules($storageId, $this->getRelevantPaths($path)); $rules[$path] = $this->filterApplicableRulesToUser($newRules); @@ -228,15 +240,15 @@ private function calculatePermissionsForPath(array $rules): int { /** * Get the combined "lowest" permissions for an entire directory tree */ - public function getPermissionsForTree(string $path): int { + public function getPermissionsForTree(int $storageId, string $path): int { $path = ltrim($path, '/'); - $rules = $this->ruleManager->getRulesForPrefix($this->user, $this->getRootStorageId(), $path); + $rules = $this->ruleManager->getRulesForPrefix($this->user, $storageId, $path); if ($this->inheritMergePerUser) { $pathsWithRules = array_keys($rules); $permissions = Constants::PERMISSION_ALL; foreach ($pathsWithRules as $path) { - $permissions &= $this->getACLPermissionsForPath($path); + $permissions &= $this->getACLPermissionsForPath($storageId, $path); } return $permissions; } else { @@ -247,8 +259,8 @@ public function getPermissionsForTree(string $path): int { } } - public function preloadRulesForFolder(string $path): void { - $this->ruleManager->getRulesForFilesByParent($this->user, $this->getRootStorageId(), $path); + public function preloadRulesForFolder(int $storageId, string $path): void { + $this->ruleManager->getRulesForFilesByParent($this->user, $storageId, $path); } /** diff --git a/lib/ACL/ACLManagerFactory.php b/lib/ACL/ACLManagerFactory.php index 80cdce2bc..391e55bf2 100644 --- a/lib/ACL/ACLManagerFactory.php +++ b/lib/ACL/ACLManagerFactory.php @@ -21,19 +21,16 @@ public function __construct( private readonly IAppConfig $config, private readonly LoggerInterface $logger, private readonly IUserMappingManager $userMappingManager, - private readonly \Closure $rootFolderProvider, ) { } - public function getACLManager(IUser $user, ?int $rootStorageId = null): ACLManager { + public function getACLManager(IUser $user): ACLManager { return new ACLManager( $this->ruleManager, $this->trashManager, $this->userMappingManager, $this->logger, $user, - $this->rootFolderProvider, - $rootStorageId, $this->config->getValueString('groupfolders', 'acl-inherit-per-user', 'false') === 'true', ); } diff --git a/lib/ACL/ACLStorageWrapper.php b/lib/ACL/ACLStorageWrapper.php index d8379dd05..a3856d4bf 100644 --- a/lib/ACL/ACLStorageWrapper.php +++ b/lib/ACL/ACLStorageWrapper.php @@ -19,15 +19,17 @@ class ACLStorageWrapper extends Wrapper implements IConstructableStorage { private readonly ACLManager $aclManager; private readonly bool $inShare; + private int $storageId; public function __construct($arguments) { parent::__construct($arguments); $this->aclManager = $arguments['acl_manager']; $this->inShare = $arguments['in_share']; + $this->storageId = $arguments['storage_id']; } private function getACLPermissionsForPath(string $path): int { - $permissions = $this->aclManager->getACLPermissionsForPath($path); + $permissions = $this->aclManager->getACLPermissionsForPath($this->storageId, $path); // if there is no read permissions, than deny everything if ($this->inShare) { @@ -151,7 +153,7 @@ public function unlink(string $path): bool { * This check is fairly expensive so we only do it for the actual delete and not metadata operations */ private function canDeleteTree(string $path): int { - return $this->aclManager->getPermissionsForTree($path) & Constants::PERMISSION_DELETE; + return $this->aclManager->getPermissionsForTree($this->storageId, $path) & Constants::PERMISSION_DELETE; } public function file_put_contents(string $path, mixed $data): int|float|false { diff --git a/lib/ACL/RuleManager.php b/lib/ACL/RuleManager.php index 7997c544f..59b8513d2 100644 --- a/lib/ACL/RuleManager.php +++ b/lib/ACL/RuleManager.php @@ -26,6 +26,9 @@ public function __construct( } private function createRule(array $data): ?Rule { + if (!isset($data['mapping_type'])) { + return null; + } $mapping = $this->userMappingManager->mappingFromId($data['mapping_type'], $data['mapping_id']); if ($mapping) { return new Rule( @@ -105,6 +108,31 @@ public function getRulesForFilesByPath(IUser $user, int $storageId, array $fileP return $this->rulesByPath($rows, $result); } + /** + * @param int[] $fileIds + * @return array + */ + public function getRulesForFilesByIds(IUser $user, array $fileIds): array { + $userMappings = $this->userMappingManager->getMappingsForUser($user); + + $rows = []; + foreach (array_chunk($fileIds, 1000) as $chunk) { + $query = $this->connection->getQueryBuilder(); + $query->select(['f.fileid', 'a.mapping_type', 'a.mapping_id', 'a.mask', 'a.permissions', 'f.path']) + ->from('filecache', 'f') + ->leftJoin('f', 'group_folders_acl', 'a', $query->expr()->eq('f.fileid', 'a.fileid')) + ->where($query->expr()->in('f.fileid', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($query->expr()->orX(...array_map(fn (IUserMapping $userMapping): ICompositeExpression => $query->expr()->andX( + $query->expr()->eq('a.mapping_type', $query->createNamedParameter($userMapping->getType())), + $query->expr()->eq('a.mapping_id', $query->createNamedParameter($userMapping->getId())) + ), $userMappings))); + + $rows = array_merge($rows, $query->executeQuery()->fetchAll()); + } + + return $this->rulesByFileId($rows); + } + /** * @return array */ @@ -199,6 +227,24 @@ private function rulesByPath(array $rows, array $result = []): array { return $result; } + private function rulesByFileId(array $rows): array { + $result = []; + foreach ($rows as $row) { + if (!isset($result[$row['path']])) { + $result[$row['path']] = []; + } + + $rule = $this->createRule($row); + if ($rule) { + $result[$row['path']][] = $rule; + } + } + + ksort($result); + + return $result; + } + /** * @return array */ diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 6576ac0f0..8db671213 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -15,7 +15,6 @@ use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent; use OCA\Files_Trashbin\Expiration; use OCA\GroupFolders\ACL\ACLManagerFactory; -use OCA\GroupFolders\ACL\RuleManager; use OCA\GroupFolders\ACL\UserMapping\IUserMappingManager; use OCA\GroupFolders\ACL\UserMapping\UserMappingManager; use OCA\GroupFolders\AuthorizedAdminSettingMiddleware; @@ -31,6 +30,7 @@ use OCA\GroupFolders\Listeners\CircleDestroyedEventListener; use OCA\GroupFolders\Listeners\LoadAdditionalScriptsListener; use OCA\GroupFolders\Listeners\NodeRenamedListener; +use OCA\GroupFolders\Mount\FolderStorageManager; use OCA\GroupFolders\Mount\MountProvider; use OCA\GroupFolders\Trash\TrashBackend; use OCA\GroupFolders\Trash\TrashManager; @@ -54,7 +54,6 @@ use OCP\Files\Storage\IStorageFactory; use OCP\Group\Events\GroupDeletedEvent; use OCP\IAppConfig; -use OCP\ICacheFactory; use OCP\IDBConnection; use OCP\IRequest; use OCP\IUserManager; @@ -118,7 +117,7 @@ public function register(IRegistrationContext $context): void { $c->get(IRequest::class), $c->get(IMountProviderCollection::class), $c->get(IDBConnection::class), - $c->get(ICacheFactory::class)->createLocal('groupfolders'), + $c->get(FolderStorageManager::class), $allowRootShare, $enableEncryption ); @@ -216,19 +215,6 @@ public function register(IRegistrationContext $context): void { return new ExpireGroupPlaceholder($c->get(ITimeFactory::class)); }); - $context->registerService(ACLManagerFactory::class, function (ContainerInterface $c): ACLManagerFactory { - $rootFolderProvider = fn (): \OCP\Files\IRootFolder => $c->get(IRootFolder::class); - - return new ACLManagerFactory( - $c->get(RuleManager::class), - $c->get(TrashManager::class), - $c->get(IAppConfig::class), - $c->get(LoggerInterface::class), - $c->get(IUserMappingManager::class), - $rootFolderProvider - ); - }); - $context->registerServiceAlias(IUserMappingManager::class, UserMappingManager::class); $context->registerMiddleware(AuthorizedAdminSettingMiddleware::class); diff --git a/lib/BackgroundJob/ExpireGroupVersions.php b/lib/BackgroundJob/ExpireGroupVersions.php index 1ae23153a..304e58497 100644 --- a/lib/BackgroundJob/ExpireGroupVersions.php +++ b/lib/BackgroundJob/ExpireGroupVersions.php @@ -9,6 +9,7 @@ namespace OCA\GroupFolders\BackgroundJob; use OCA\GroupFolders\AppInfo\Application; +use OCA\GroupFolders\Folder\FolderDefinition; use OCA\GroupFolders\Folder\FolderManager; use OCA\GroupFolders\Versions\GroupVersionsExpireManager; use OCP\AppFramework\Utility\ITimeFactory; @@ -39,7 +40,7 @@ public function __construct( */ protected function run(mixed $argument): void { $lastFolder = $this->appConfig->getValueInt(Application::APP_ID, 'cron_last_folder_index', 0); - $folders = $this->folderManager->getAllFolders(); + $folders = $this->folderManager->getAllFoldersWithSize(); $folderCount = count($folders); $currentRunHour = (int)date('G', $this->time->getTime()); @@ -63,7 +64,7 @@ protected function run(mixed $argument): void { // Determine the set of folders to process $folderSet = array_slice($folders, $lastFolder, $toDo); - $folderIDs = array_map(fn (array $folder): int => $folder['id'], $folderSet); + $folderIDs = array_map(fn (FolderDefinition $folder): int => $folder->id, $folderSet); // Log and start the expiration process $this->logger->debug('Expiring versions for ' . count($folderSet) . ' folders', ['app' => 'cron', 'folders' => $folderIDs]); diff --git a/lib/Command/ACL.php b/lib/Command/ACL.php index 06fad5dad..ad1f59019 100644 --- a/lib/Command/ACL.php +++ b/lib/Command/ACL.php @@ -12,6 +12,8 @@ use OCA\GroupFolders\ACL\Rule; use OCA\GroupFolders\ACL\RuleManager; use OCA\GroupFolders\ACL\UserMapping\UserMapping; +use OCA\GroupFolders\Folder\FolderDefinition; +use OCA\GroupFolders\Folder\FolderDefinitionWithPermissions; use OCA\GroupFolders\Folder\FolderManager; use OCA\GroupFolders\Mount\MountProvider; use OCP\Constants; @@ -61,9 +63,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($input->getOption('enable')) { - $this->folderManager->setFolderACL($folder['id'], true); + $this->folderManager->setFolderACL($folder->id, true); } elseif ($input->getOption('disable')) { - $this->folderManager->setFolderACL($folder['id'], false); + $this->folderManager->setFolderACL($folder->id, false); } elseif ($input->getOption('test')) { if ($input->getOption('user') && ($input->getArgument('path'))) { $mappingId = $input->getOption('user'); @@ -73,13 +75,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int return -1; } - $jailPath = $this->mountProvider->getJailPath((int)$folder['id']); + $jailPath = $this->mountProvider->getJailPath($folder->id); $path = $input->getArgument('path'); $aclManager = $this->aclManagerFactory->getACLManager($user); - if ($this->folderManager->getFolderPermissionsForUser($user, $folder['id']) === 0) { + if ($this->folderManager->getFolderPermissionsForUser($user, $folder->id) === 0) { $permissions = 0; } else { - $permissions = $aclManager->getACLPermissionsForPath($jailPath . rtrim('/' . $path, '/')); + $permissions = $aclManager->getACLPermissionsForPath($folder->storageId, $jailPath . rtrim('/' . $path, '/')); } $permissionString = Rule::formatRulePermissions(Constants::PERMISSION_ALL, $permissions); $output->writeln($permissionString); @@ -89,8 +91,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln('--user and options needs to be set for permissions testing'); return -3; } - } elseif (!$folder['acl']) { - $output->writeln('Advanced permissions not enabled for folder: ' . $folder['id'] . ''); + } elseif (!$folder->acl) { + $output->writeln('Advanced permissions not enabled for folder: ' . $folder->id . ''); return -2; } elseif ( !$input->getArgument('path') @@ -102,10 +104,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->printPermissions($input, $output, $folder); } elseif ($input->getOption('manage-add') && ($input->getOption('user') || $input->getOption('group') || $input->getOption('team'))) { [$mappingType, $mappingId] = $this->convertMappingOptions($input); - $this->folderManager->setManageACL($folder['id'], $mappingType, $mappingId, true); + $this->folderManager->setManageACL($folder->id, $mappingType, $mappingId, true); } elseif ($input->getOption('manage-remove') && ($input->getOption('user') || $input->getOption('group') || $input->getOption('team'))) { [$mappingType, $mappingId] = $this->convertMappingOptions($input); - $this->folderManager->setManageACL($folder['id'], $mappingType, $mappingId, false); + $this->folderManager->setManageACL($folder->id, $mappingType, $mappingId, false); } elseif (!$input->getArgument('path')) { $output->writeln(' argument has to be set when not using --enable or --disable'); return -3; @@ -133,13 +135,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $mount = $this->mountProvider->getMount( - $folder['id'], - '/dummy/files/' . $folder['mount_point'], - Constants::PERMISSION_ALL, - $folder['quota'], - null, - null, - $folder['acl'] + FolderDefinitionWithPermissions::fromFolder($folder, $folder->rootCacheEntry, Constants::PERMISSION_ALL), + '/dummy/files/' . $folder->mountPoint, ); $id = $mount->getStorage()->getCache()->getId($path); if ($id === -1) { @@ -189,8 +186,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - private function printPermissions(InputInterface $input, OutputInterface $output, array $folder): void { - $jailPath = $this->mountProvider->getJailPath((int)$folder['id']); + private function printPermissions(InputInterface $input, OutputInterface $output, FolderDefinition $folder): void { + $jailPath = $this->mountProvider->getJailPath($folder->id); $rules = $this->ruleManager->getAllRulesForPrefix( $this->rootFolder->getMountPoint()->getNumericStorageId(), $jailPath diff --git a/lib/Command/Create.php b/lib/Command/Create.php index 38bc08df5..eb4b84522 100644 --- a/lib/Command/Create.php +++ b/lib/Command/Create.php @@ -41,7 +41,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Check if mount point already exists $folders = $this->folderManager->getAllFolders(); foreach ($folders as $folder) { - if ($folder['mount_point'] === $name) { + if ($folder->mountPoint === $name) { $output->writeln('A Folder with the name ' . $name . ' already exists'); return 1; } diff --git a/lib/Command/Delete.php b/lib/Command/Delete.php index 3033ab0b9..bd5df756d 100644 --- a/lib/Command/Delete.php +++ b/lib/Command/Delete.php @@ -31,10 +31,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion('Are you sure you want to delete the Team folder ' . $folder['mount_point'] . ' and all files within, this cannot be undone (y/N).', false); + $question = new ConfirmationQuestion('Are you sure you want to delete the Team folder ' . $folder->mountPoint . ' and all files within, this cannot be undone (y/N).', false); if ($input->getOption('force') || $helper->ask($input, $output, $question)) { - $folderMount = $this->mountProvider->getFolder($folder['id']); - $this->folderManager->removeFolder($folder['id']); + $folderMount = $this->mountProvider->getFolder($folder->id); + $this->folderManager->removeFolder($folder->id); $folderMount->delete(); } diff --git a/lib/Command/ExpireGroup/ExpireGroupVersions.php b/lib/Command/ExpireGroup/ExpireGroupVersions.php index 63d53fcf8..4363ddc48 100644 --- a/lib/Command/ExpireGroup/ExpireGroupVersions.php +++ b/lib/Command/ExpireGroup/ExpireGroupVersions.php @@ -36,7 +36,7 @@ protected function configure(): void { protected function execute(InputInterface $input, OutputInterface $output): int { $this->eventDispatcher->addListener(GroupVersionsExpireEnterFolderEvent::class, function (GroupVersionsExpireEnterFolderEvent $event) use ($output): void { - $output->writeln("Expiring version in '{$event->folder['mount_point']}'"); + $output->writeln("Expiring version in '{$event->folder->mountPoint}'"); }); $this->eventDispatcher->addListener(GroupVersionsExpireDeleteVersionEvent::class, function (GroupVersionsExpireDeleteVersionEvent $event) use ($output): void { $id = $event->version->getRevisionId(); diff --git a/lib/Command/FolderCommand.php b/lib/Command/FolderCommand.php index 9a9800196..f1acf23c8 100644 --- a/lib/Command/FolderCommand.php +++ b/lib/Command/FolderCommand.php @@ -10,6 +10,7 @@ use OC\Core\Command\Base; use OCA\GroupFolders\Folder\FolderManager; +use OCA\GroupFolders\Folder\FolderWithMappingsAndCache; use OCA\GroupFolders\Mount\MountProvider; use OCP\Files\IRootFolder; use Symfony\Component\Console\Input\InputInterface; @@ -17,8 +18,6 @@ /** * Base command for commands asking the user for a folder id. - * - * @psalm-import-type InternalFolderOut from FolderManager */ abstract class FolderCommand extends Base { @@ -30,10 +29,7 @@ public function __construct( parent::__construct(); } - /** - * @return ?InternalFolderOut - */ - protected function getFolder(InputInterface $input, OutputInterface $output): ?array { + protected function getFolder(InputInterface $input, OutputInterface $output): ?FolderWithMappingsAndCache { $folderId = (int)$input->getArgument('folder_id'); if ((string)$folderId !== $input->getArgument('folder_id')) { // Protect against removing folderId === 0 when typing a string (e.g. folder name instead of folder id) @@ -42,14 +38,7 @@ protected function getFolder(InputInterface $input, OutputInterface $output): ?a return null; } - $rootStorageId = $this->rootFolder->getMountPoint()->getNumericStorageId(); - if ($rootStorageId === null) { - $output->writeln('Root storage id not found'); - return null; - } - - - $folder = $this->folderManager->getFolder($folderId, $rootStorageId); + $folder = $this->folderManager->getFolder($folderId); if ($folder === null) { $output->writeln('Folder not found: ' . $folderId . ''); return null; diff --git a/lib/Command/Group.php b/lib/Command/Group.php index 172b9e32d..17015a353 100644 --- a/lib/Command/Group.php +++ b/lib/Command/Group.php @@ -56,17 +56,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int $groupString = $input->getArgument('group'); $group = $this->groupManager->get($groupString); if ($input->getOption('delete')) { - $this->folderManager->removeApplicableGroup($folder['id'], $groupString); + $this->folderManager->removeApplicableGroup($folder->id, $groupString); return 0; } elseif ($group || $this->folderManager->isACircle($groupString)) { $permissionsString = $input->getArgument('permissions'); $permissions = $this->getNewPermissions($permissionsString); if ($permissions) { - if (!isset($folder['groups'][$groupString])) { - $this->folderManager->addApplicableGroup($folder['id'], $groupString); + if (!isset($folder->groups[$groupString])) { + $this->folderManager->addApplicableGroup($folder->id, $groupString); } - $this->folderManager->setGroupPermissions($folder['id'], $groupString, $permissions); + $this->folderManager->setGroupPermissions($folder->id, $groupString, $permissions); return 0; } diff --git a/lib/Command/ListCommand.php b/lib/Command/ListCommand.php index 925c5e919..a5eb32535 100644 --- a/lib/Command/ListCommand.php +++ b/lib/Command/ListCommand.php @@ -9,9 +9,10 @@ namespace OCA\GroupFolders\Command; use OC\Core\Command\Base; +use OCA\GroupFolders\Folder\FolderDefinition; use OCA\GroupFolders\Folder\FolderManager; +use OCA\GroupFolders\Folder\FolderWithMappingsAndCache; use OCP\Constants; -use OCP\Files\IRootFolder; use OCP\IGroupManager; use OCP\IUserManager; use Symfony\Component\Console\Helper\Table; @@ -31,7 +32,6 @@ class ListCommand extends Base { public function __construct( private readonly FolderManager $folderManager, - private readonly IRootFolder $rootFolder, private readonly IGroupManager $groupManager, private readonly IUserManager $userManager, ) { @@ -54,12 +54,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $groupNames[$group->getGID()] = $group->getDisplayName(); } - $rootStorageId = $this->rootFolder->getMountPoint()->getNumericStorageId(); - if ($rootStorageId === null) { - $output->writeln('Root storage id not found'); - return 1; - } - if ($userId) { $user = $this->userManager->get($userId); if (!$user) { @@ -67,12 +61,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - $folders = $this->folderManager->getAllFoldersForUserWithSize($rootStorageId, $user); + $folders = $this->folderManager->getAllFoldersForUserWithSize($user); } else { - $folders = $this->folderManager->getAllFoldersWithSize($rootStorageId); + $folders = $this->folderManager->getAllFoldersWithSize(); } - usort($folders, fn (array $a, array $b): int => $a['id'] - $b['id']); + usort($folders, fn (FolderDefinition $a, FolderDefinition $b): int => $a->id - $b->id); $outputType = $input->getOption('output'); if (count($folders) === 0) { @@ -86,7 +80,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($outputType === self::OUTPUT_FORMAT_JSON || $outputType === self::OUTPUT_FORMAT_JSON_PRETTY) { - foreach ($folders as &$folder) { + $formatted = array_map(fn (FolderWithMappingsAndCache $folder): array => $folder->toArray(), $folders); + foreach ($formatted as &$folder) { + $folder['size'] = $folder['root_cache_entry']->getSize(); + unset($folder['root_cache_entry']); $folder['group_details'] = $folder['groups']; $folder['groups'] = array_map(fn (array $group): int => $group['permissions'], $folder['groups']); } @@ -95,21 +92,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int } else { $table = new Table($output); $table->setHeaders(['Folder Id', 'Name', 'Groups', 'Quota', 'Size', 'Advanced Permissions', 'Manage advanced permissions']); - $table->setRows(array_map(function (array $folder) use ($groupNames): array { - $folder['size'] = \OCP\Util::humanFileSize($folder['size']); - $folder['quota'] = ($folder['quota'] > 0) ? \OCP\Util::humanFileSize($folder['quota']) : 'Unlimited'; + $table->setRows(array_map(function (FolderWithMappingsAndCache $folder) use ($groupNames): array { + $formatted = ['id' => $folder->id, 'name' => $folder->mountPoint]; + $formatted['quota'] = ($folder->quota > 0) ? \OCP\Util::humanFileSize($folder->quota) : 'Unlimited'; $groupStrings = array_map(function (string $groupId, array $entry) use ($groupNames): string { [$permissions, $displayName] = [$entry['permissions'], $entry['displayName']]; $groupName = array_key_exists($groupId, $groupNames) && ($groupNames[$groupId] !== $groupId) ? $groupNames[$groupId] . ' (' . $groupId . ')' : $displayName; return $groupName . ': ' . $this->permissionsToString($permissions); - }, array_keys($folder['groups']), array_values($folder['groups'])); - $folder['groups'] = implode("\n", $groupStrings); - $folder['acl'] = $folder['acl'] ? 'Enabled' : 'Disabled'; - $manageStrings = array_map(fn (array $manage): string => $manage['displayname'] . ' (' . $manage['type'] . ')', $folder['manage']); - $folder['manage'] = implode("\n", $manageStrings); - - return $folder; + }, array_keys($folder->groups), array_values($folder->groups)); + $formatted['groups'] = implode("\n", $groupStrings); + $formatted['size'] = $folder->rootCacheEntry->getSize(); + $formatted['acl'] = $folder->acl ? 'Enabled' : 'Disabled'; + $manageStrings = array_map(fn (array $manage): string => $manage['displayname'] . ' (' . $manage['type'] . ')', $folder->manage); + $formatted['manage'] = implode("\n", $manageStrings); + + return $formatted; }, $folders)); $table->render(); } diff --git a/lib/Command/Quota.php b/lib/Command/Quota.php index 4e777f949..af817a3de 100644 --- a/lib/Command/Quota.php +++ b/lib/Command/Quota.php @@ -38,7 +38,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $quotaString = strtolower($quotaString); $quota = ($quotaString === 'unlimited') ? FileInfo::SPACE_UNLIMITED : \OCP\Util::computerFileSize($quotaString); if ($quota) { - $this->folderManager->setFolderQuota($folder['id'], (int)$quota); + $this->folderManager->setFolderQuota($folder->id, (int)$quota); return 0; } diff --git a/lib/Command/Rename.php b/lib/Command/Rename.php index e0a12dd8c..859929ea1 100644 --- a/lib/Command/Rename.php +++ b/lib/Command/Rename.php @@ -36,7 +36,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Check if the name actually changed - if ($folder['mount_point'] === $name) { + if ($folder->mountPoint === $name) { $output->writeln('The name is already set to ' . $name); return 0; } @@ -44,13 +44,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Check if mount point already exists $folders = $this->folderManager->getAllFolders(); foreach ($folders as $existingFolder) { - if ($existingFolder['mount_point'] === $name) { + if ($existingFolder->mountPoint === $name) { $output->writeln('A Folder with the name ' . $name . ' already exists'); return 1; } } - $this->folderManager->renameFolder($folder['id'], $input->getArgument('name')); + $this->folderManager->renameFolder($folder->id, $input->getArgument('name')); return 0; } diff --git a/lib/Command/Scan.php b/lib/Command/Scan.php index 5b1519e4e..c5c68c1b5 100644 --- a/lib/Command/Scan.php +++ b/lib/Command/Scan.php @@ -9,6 +9,7 @@ namespace OCA\GroupFolders\Command; use OC\Files\ObjectStore\ObjectStoreScanner; +use OCA\GroupFolders\Folder\FolderDefinitionWithPermissions; use OCP\Constants; use OCP\Files\Cache\IScanner; use Symfony\Component\Console\Helper\Table; @@ -60,14 +61,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($all) { - $folders = $this->folderManager->getAllFolders(); + $folders = $this->folderManager->getAllFoldersWithSize(); } else { $folder = $this->getFolder($input, $output); if ($folder === null) { return -1; } - $folders = [$folder['id'] => $folder]; + $folders = [$folder->id => $folder]; } $inputPath = $input->getOption('path'); @@ -86,13 +87,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $stats = []; foreach ($folders as $folder) { - $folderId = $folder['id']; + $folderId = $folder->id; $statsRow = [$folderId, 0, 0, 0]; - $mount = $this->mountProvider->getMount($folder['id'], '/' . $folder['mount_point'], Constants::PERMISSION_ALL, $folder['quota']); + $folderWithPermissions = FolderDefinitionWithPermissions::fromFolder($folder, $folder->rootCacheEntry, Constants::PERMISSION_ALL); + $mount = $this->mountProvider->getMount($folderWithPermissions, '/' . $folder->mountPoint); /** @var IScanner&\OC\Hooks\BasicEmitter $scanner */ $scanner = $mount->getStorage()->getScanner(); - $output->writeln("Scanning Team folder with id\t{$folder['id']}", OutputInterface::VERBOSITY_VERBOSE); + $output->writeln("Scanning Team folder with id\t{$folder->id}", OutputInterface::VERBOSITY_VERBOSE); if ($scanner instanceof ObjectStoreScanner) { $output->writeln('Scanning Team folders using an object store as primary storage is not supported.'); return -1; diff --git a/lib/Command/Trashbin/Cleanup.php b/lib/Command/Trashbin/Cleanup.php index 12391d3fe..8abc2ff0d 100644 --- a/lib/Command/Trashbin/Cleanup.php +++ b/lib/Command/Trashbin/Cleanup.php @@ -9,9 +9,12 @@ namespace OCA\GroupFolders\Command\Trashbin; use OC\Core\Command\Base; +use OCA\GroupFolders\Folder\FolderDefinitionWithPermissions; use OCA\GroupFolders\Folder\FolderManager; +use OCA\GroupFolders\Folder\FolderWithMappingsAndCache; use OCA\GroupFolders\Trash\TrashBackend; use OCP\App\IAppManager; +use OCP\Constants; use OCP\Server; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; @@ -50,18 +53,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); - $folders = $this->folderManager->getAllFolders(); + $folders = $this->folderManager->getAllFoldersWithSize(); + $folders = array_map(function (FolderWithMappingsAndCache $folder): FolderDefinitionWithPermissions { + return FolderDefinitionWithPermissions::fromFolder($folder, $folder->rootCacheEntry, Constants::PERMISSION_ALL); + }, $folders); if ($input->getArgument('folder_id') !== null) { $folderId = (int)$input->getArgument('folder_id'); foreach ($folders as $folder) { - if ($folder['id'] === $folderId) { + if ($folder->id === $folderId) { $question = new ConfirmationQuestion('Are you sure you want to empty the trashbin of your Team folder with id ' . $folderId . ', this can not be undone (y/N).', false); if (!$input->getOption('force') && !$helper->ask($input, $output, $question)) { return -1; } - $this->trashBackend->cleanTrashFolder($folder['id']); + $this->trashBackend->cleanTrashFolder($folder); return 0; } @@ -77,7 +83,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } foreach ($folders as $folder) { - $this->trashBackend->cleanTrashFolder($folder['id']); + $this->trashBackend->cleanTrashFolder($folder); } } diff --git a/lib/Controller/FolderController.php b/lib/Controller/FolderController.php index 495acde37..1a3081b5e 100644 --- a/lib/Controller/FolderController.php +++ b/lib/Controller/FolderController.php @@ -11,6 +11,7 @@ use OC\AppFramework\OCS\V1Response; use OCA\GroupFolders\Attribute\RequireGroupFolderAdmin; use OCA\GroupFolders\Folder\FolderManager; +use OCA\GroupFolders\Folder\FolderWithMappingsAndCache; use OCA\GroupFolders\Mount\MountProvider; use OCA\GroupFolders\ResponseDefinitions; use OCA\GroupFolders\Service\DelegationService; @@ -36,7 +37,6 @@ * @psalm-import-type GroupFoldersCircle from ResponseDefinitions * @psalm-import-type GroupFoldersUser from ResponseDefinitions * @psalm-import-type GroupFoldersFolder from ResponseDefinitions - * @psalm-import-type InternalFolderOut from FolderManager */ class FolderController extends OCSController { private readonly ?IUser $user; @@ -86,15 +86,19 @@ private function filterNonAdminFolder(array $folder): ?array { } /** - * @param InternalFolderOut $folder * @return GroupFoldersFolder */ - private function formatFolder(array $folder): array { - // keep compatibility with the old 'groups' field - $folder['group_details'] = $folder['groups']; - $folder['groups'] = array_map(fn (array $group): int => $group['permissions'], $folder['groups']); - - return $folder; + private function formatFolder(FolderWithMappingsAndCache $folder): array { + return [ + 'id' => $folder->id, + 'mount_point' => $folder->mountPoint, + 'quota' => $folder->quota, + 'acl' => $folder->acl, + 'size' => $folder->rootCacheEntry->getSize(), + 'groups' => array_map(fn (array $group): int => $group['permissions'], $folder->groups), + 'group_details' => $folder->groups, + 'manage' => $folder->manage, + ]; } /** @@ -123,7 +127,7 @@ public function getFolders(bool $applicable = false, int $offset = 0, ?int $limi } $folders = []; - foreach ($this->manager->getAllFoldersWithSize($storageId) as $id => $folder) { + foreach ($this->manager->getAllFoldersWithSize() as $id => $folder) { // Make them string-indexed for OpenAPI JSON output $folders[(string)$id] = $this->formatFolder($folder); } @@ -200,10 +204,7 @@ public function getFolder(int $id): DataResponse { throw new OCSNotFoundException(); } - $this->checkFolderExists($id); - - /** @var InternalFolderOut */ - $folder = $this->manager->getFolder($id, $storageId); + $folder = $this->checkedGetFolder($id); $folder = $this->formatFolder($folder); if (!$this->delegationService->hasApiAccess()) { @@ -216,21 +217,13 @@ public function getFolder(int $id): DataResponse { return new DataResponse($folder); } - /** - * @return DataResponse, array{}>|null - */ - private function checkFolderExists(int $id): ?DataResponse { - $storageId = $this->getRootFolderStorageId(); - if ($storageId === null) { - throw new OCSNotFoundException('Groupfolder not found'); - } - - $folder = $this->manager->getFolder($id, $storageId); + private function checkedGetFolder(int $id): FolderWithMappingsAndCache { + $folder = $this->manager->getFolder($id); if ($folder === null) { throw new OCSNotFoundException('Groupfolder not found'); } - return null; + return $folder; } private function checkMountPointExists(string $mountpoint): ?DataResponse { @@ -241,7 +234,7 @@ private function checkMountPointExists(string $mountpoint): ?DataResponse { $folders = $this->manager->getAllFolders(); foreach ($folders as $folder) { - if ($folder['mount_point'] === $mountpoint) { + if ($folder->mountPoint === $mountpoint) { throw new OCSBadRequestException('Mount point already exists'); } } @@ -276,10 +269,7 @@ public function addFolder(string $mountpoint): DataResponse { $this->checkMountPointExists(trim($mountpoint)); $id = $this->manager->createFolder(trim($mountpoint)); - $this->checkFolderExists($id); - - /** @var InternalFolderOut */ - $folder = $this->manager->getFolder($id, $storageId); + $folder = $this->checkedGetFolder($id); return new DataResponse($this->formatFolder($folder)); } @@ -299,7 +289,7 @@ public function addFolder(string $mountpoint): DataResponse { #[NoAdminRequired] #[FrontpageRoute(verb: 'DELETE', url: '/folders/{id}')] public function removeFolder(int $id): DataResponse { - $this->checkFolderExists($id); + $this->checkedGetFolder($id); /** @var Folder */ $folder = $this->mountProvider->getFolder($id); @@ -327,12 +317,9 @@ public function removeFolder(int $id): DataResponse { public function setMountPoint(int $id, string $mountPoint): DataResponse { $this->manager->renameFolder($id, trim($mountPoint)); - $this->checkFolderExists($id); + $folder = $this->checkedGetFolder($id); $this->checkMountPointExists(trim($mountPoint)); - /** @var InternalFolderOut */ - $folder = $this->manager->getFolder($id); - return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } @@ -351,12 +338,11 @@ public function setMountPoint(int $id, string $mountPoint): DataResponse { #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders/{id}/groups')] public function addGroup(int $id, string $group): DataResponse { - $this->checkFolderExists($id); + $this->checkedGetFolder($id); $this->manager->addApplicableGroup($id, $group); - /** @var InternalFolderOut */ - $folder = $this->manager->getFolder($id); + $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } @@ -376,12 +362,11 @@ public function addGroup(int $id, string $group): DataResponse { #[NoAdminRequired] #[FrontpageRoute(verb: 'DELETE', url: '/folders/{id}/groups/{group}', requirements: ['group' => '.+'])] public function removeGroup(int $id, string $group): DataResponse { - $this->checkFolderExists($id); + $this->checkedGetFolder($id); $this->manager->removeApplicableGroup($id, $group); - /** @var InternalFolderOut */ - $folder = $this->manager->getFolder($id); + $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } @@ -402,12 +387,11 @@ public function removeGroup(int $id, string $group): DataResponse { #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders/{id}/groups/{group}', requirements: ['group' => '.+'])] public function setPermissions(int $id, string $group, int $permissions): DataResponse { - $this->checkFolderExists($id); + $this->checkedGetFolder($id); $this->manager->setGroupPermissions($id, $group, $permissions); - /** @var InternalFolderOut */ - $folder = $this->manager->getFolder($id); + $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } @@ -429,12 +413,11 @@ public function setPermissions(int $id, string $group, int $permissions): DataRe #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders/{id}/manageACL')] public function setManageACL(int $id, string $mappingType, string $mappingId, bool $manageAcl): DataResponse { - $this->checkFolderExists($id); + $this->checkedGetFolder($id); $this->manager->setManageACL($id, $mappingType, $mappingId, $manageAcl); - /** @var InternalFolderOut */ - $folder = $this->manager->getFolder($id); + $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } @@ -454,12 +437,11 @@ public function setManageACL(int $id, string $mappingType, string $mappingId, bo #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders/{id}/quota')] public function setQuota(int $id, int $quota): DataResponse { - $this->checkFolderExists($id); + $this->checkedGetFolder($id); $this->manager->setFolderQuota($id, $quota); - /** @var InternalFolderOut */ - $folder = $this->manager->getFolder($id); + $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } @@ -479,12 +461,11 @@ public function setQuota(int $id, int $quota): DataResponse { #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders/{id}/acl')] public function setACL(int $id, bool $acl): DataResponse { - $this->checkFolderExists($id); + $this->checkedGetFolder($id); $this->manager->setFolderACL($id, $acl); - /** @var InternalFolderOut */ - $folder = $this->manager->getFolder($id); + $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } @@ -506,27 +487,23 @@ public function setACL(int $id, bool $acl): DataResponse { #[NoAdminRequired] #[FrontpageRoute(verb: 'POST', url: '/folders/{id}/mountpoint')] public function renameFolder(int $id, string $mountpoint): DataResponse { - $this->checkFolderExists($id); + $this->checkedGetFolder($id); // Check if the new mountpoint is valid if (empty($mountpoint)) { throw new OCSBadRequestException('Mount point cannot be empty'); } - // Check if we actually need to do anything - /** @var InternalFolderOut */ - $folder = $this->manager->getFolder($id); + $folder = $this->checkedGetFolder($id); - if ($folder['mount_point'] === trim($mountpoint)) { + if ($folder->mountPoint === trim($mountpoint)) { return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } $this->checkMountPointExists(trim($mountpoint)); $this->manager->renameFolder($id, trim($mountpoint)); - // Get the new folder data - /** @var InternalFolderOut */ - $folder = $this->manager->getFolder($id); + $folder = $this->checkedGetFolder($id); return new DataResponse(['success' => true, 'folder' => $this->formatFolder($folder)]); } diff --git a/lib/DAV/ACLPlugin.php b/lib/DAV/ACLPlugin.php index 5b72c777a..fc9312561 100644 --- a/lib/DAV/ACLPlugin.php +++ b/lib/DAV/ACLPlugin.php @@ -227,7 +227,7 @@ public function propPatch(string $path, PropPatch $propPatch): void { } $aclManager = $this->aclManagerFactory->getACLManager($this->user); - $newPermissions = $aclManager->testACLPermissionsForPath($path, $rules); + $newPermissions = $aclManager->testACLPermissionsForPath($mount->getNumericStorageId(), $path, $rules); if (!($newPermissions & Constants::PERMISSION_READ)) { throw new BadRequest($this->l10n->t('You cannot remove your own read permission.')); } diff --git a/lib/DAV/GroupFoldersHome.php b/lib/DAV/GroupFoldersHome.php index 663679b8d..a909e5fa0 100644 --- a/lib/DAV/GroupFoldersHome.php +++ b/lib/DAV/GroupFoldersHome.php @@ -9,6 +9,7 @@ namespace OCA\GroupFolders\DAV; use OC\Files\Filesystem; +use OCA\GroupFolders\Folder\FolderDefinition; use OCA\GroupFolders\Folder\FolderManager; use OCP\Files\IRootFolder; use OCP\IUser; @@ -17,9 +18,6 @@ use Sabre\DAV\Exception\NotFound; use Sabre\DAV\ICollection; -/** - * @psalm-import-type InternalFolder from FolderManager - */ class GroupFoldersHome implements ICollection { public function __construct( private array $principalInfo, @@ -51,17 +49,17 @@ public function createDirectory($name): never { } /** - * @return ?InternalFolder + * @return ?FolderDefinition */ - private function getFolder(string $name): ?array { + private function getFolder(string $name): ?FolderDefinition { $storageId = $this->rootFolder->getMountPoint()->getNumericStorageId(); if ($storageId === null) { return null; } - $folders = $this->folderManager->getFoldersForUser($this->user, $storageId); + $folders = $this->folderManager->getFoldersForUser($this->user); foreach ($folders as $folder) { - if (basename($folder['mount_point']) === $name) { + if (basename($folder->mountPoint) === $name) { return $folder; } } @@ -69,19 +67,16 @@ private function getFolder(string $name): ?array { return null; } - /** - * @param InternalFolder $folder - */ - private function getDirectoryForFolder(array $folder): GroupFolderNode { + private function getDirectoryForFolder(FolderDefinition $folder): GroupFolderNode { $userHome = '/' . $this->user->getUID() . '/files'; - $node = $this->rootFolder->get($userHome . '/' . $folder['mount_point']); + $node = $this->rootFolder->get($userHome . '/' . $folder->mountPoint); $view = Filesystem::getView(); if ($view === null) { throw new RuntimeException('Unable to create view.'); } - return new GroupFolderNode($view, $node, $folder['folder_id']); + return new GroupFolderNode($view, $node, $folder->id); } public function getChild($name): GroupFolderNode { @@ -102,11 +97,11 @@ public function getChildren(): array { return []; } - $folders = $this->folderManager->getFoldersForUser($this->user, $storageId); + $folders = $this->folderManager->getFoldersForUser($this->user); // Filter out non top-level folders - $folders = array_filter($folders, function (array $folder) { - return !str_contains($folder['mount_point'], '/'); + $folders = array_filter($folders, function (FolderDefinition $folder) { + return !str_contains($folder->mountPoint, '/'); }); return array_map($this->getDirectoryForFolder(...), $folders); diff --git a/lib/Event/GroupVersionsExpireEnterFolderEvent.php b/lib/Event/GroupVersionsExpireEnterFolderEvent.php index 77929cc8c..febf1f7b6 100644 --- a/lib/Event/GroupVersionsExpireEnterFolderEvent.php +++ b/lib/Event/GroupVersionsExpireEnterFolderEvent.php @@ -8,6 +8,7 @@ namespace OCA\GroupFolders\Event; +use OCA\GroupFolders\Folder\FolderDefinition; use OCP\EventDispatcher\Event; /** @@ -15,7 +16,7 @@ */ class GroupVersionsExpireEnterFolderEvent extends Event { public function __construct( - public array $folder, + public readonly FolderDefinition $folder, ) { parent::__construct(); } diff --git a/lib/Folder/FolderDefinition.php b/lib/Folder/FolderDefinition.php new file mode 100644 index 000000000..40cebd420 --- /dev/null +++ b/lib/Folder/FolderDefinition.php @@ -0,0 +1,21 @@ + $groups + * @psalm-param list $manage + */ + public function __construct( + int $id, + string $mountPoint, + int $quota, + bool $acl, + int $storageId, + int $rootId, + public readonly array $groups, + public readonly array $manage, + ) { + parent::__construct($id, $mountPoint, $quota, $acl, $storageId, $rootId); + } + + /** + * @psalm-param array $groups + * @psalm-param list $manage + */ + public static function fromFolder(FolderDefinition $folder, array $groups, array $manage): FolderDefinitionWithMappings { + return new FolderDefinitionWithMappings( + $folder->id, + $folder->mountPoint, + $folder->quota, + $folder->acl, + $folder->storageId, + $folder->rootId, + $groups, + $manage, + ); + } + + public function toArray(): array { + return [ + 'id' => $this->id, + 'mount_point' => $this->mountPoint, + 'quota' => $this->quota, + 'acl' => $this->acl, + 'storage_id' => $this->storageId, + 'root_id' => $this->rootId, + 'groups' => $this->groups, + 'manage' => $this->manage, + ]; + } +} diff --git a/lib/Folder/FolderDefinitionWithPermissions.php b/lib/Folder/FolderDefinitionWithPermissions.php new file mode 100644 index 000000000..4b46a2aa5 --- /dev/null +++ b/lib/Folder/FolderDefinitionWithPermissions.php @@ -0,0 +1,78 @@ + $groups + * @psalm-param list $manage + */ + public function __construct( + int $id, + string $mountPoint, + int $quota, + bool $acl, + int $storageId, + int $rootId, + public readonly ICacheEntry $rootCacheEntry, + public readonly int $permissions, + ) { + parent::__construct($id, $mountPoint, $quota, $acl, $storageId, $rootId); + } + + /** + * @psalm-param array $groups + * @psalm-param list $manage + */ + public static function fromFolder(FolderDefinition $folder, ICacheEntry $rootCacheEntry, int $permissions): FolderDefinitionWithPermissions { + return new FolderDefinitionWithPermissions( + $folder->id, + $folder->mountPoint, + $folder->quota, + $folder->acl, + $folder->storageId, + $folder->rootId, + $rootCacheEntry, + $permissions, + ); + } + + public function toArray(): array { + return [ + 'id' => $this->id, + 'mount_point' => $this->mountPoint, + 'permissions' => $this->permissions, + 'quota' => $this->quota, + 'acl' => $this->acl, + 'storage_id' => $this->storageId, + 'root_id' => $this->rootId, + 'root_cache_entry' => $this->rootCacheEntry, + ]; + } + + public function withAddedPermissions(int $permissions): self { + return new FolderDefinitionWithPermissions( + $this->id, + $this->mountPoint, + $this->quota, + $this->acl, + $this->storageId, + $this->rootId, + $this->rootCacheEntry, + $this->permissions | $permissions, + ); + } +} diff --git a/lib/Folder/FolderManager.php b/lib/Folder/FolderManager.php index 84b836651..baa07af03 100644 --- a/lib/Folder/FolderManager.php +++ b/lib/Folder/FolderManager.php @@ -9,7 +9,6 @@ namespace OCA\GroupFolders\Folder; use OC\Files\Cache\Cache; -use OC\Files\Cache\CacheEntry; use OC\Files\Node\Node; use OCA\Circles\CirclesManager; use OCA\Circles\Exceptions\CircleNotFoundException; @@ -19,6 +18,7 @@ use OCA\GroupFolders\ACL\UserMapping\IUserMapping; use OCA\GroupFolders\ACL\UserMapping\IUserMappingManager; use OCA\GroupFolders\ACL\UserMapping\UserMapping; +use OCA\GroupFolders\Mount\FolderStorageManager; use OCA\GroupFolders\Mount\GroupMountPoint; use OCA\GroupFolders\ResponseDefinitions; use OCP\AutoloadNotAllowedException; @@ -46,23 +46,6 @@ * @psalm-import-type GroupFoldersUser from ResponseDefinitions * @psalm-import-type GroupFoldersAclManage from ResponseDefinitions * @psalm-import-type GroupFoldersApplicable from ResponseDefinitions - * @psalm-type InternalFolder = array{ - * folder_id: int, - * mount_point: string, - * permissions: int, - * quota: int, - * acl: bool, - * rootCacheEntry: ?CacheEntry, - * } - * @psalm-type InternalFolderOut = array{ - * id: int, - * mount_point: string, - * groups: array, - * quota: int, - * size: int, - * acl: bool, - * manage: list, - * } * @psalm-type InternalFolderMapping = array{ * folder_id: int, * mapping_type: 'user'|'group', @@ -80,73 +63,78 @@ public function __construct( private readonly IEventDispatcher $eventDispatcher, private readonly IConfig $config, private readonly IUserMappingManager $userMappingManager, + private readonly FolderStorageManager $folderStorageManager, ) { } /** - * @return array + * @return array * @throws Exception */ public function getAllFolders(): array { $applicableMap = $this->getAllApplicable(); + $folderMappings = $this->getAllFolderMappings(); $query = $this->connection->getQueryBuilder(); - $query->select('folder_id', 'mount_point', 'quota', 'acl') + $query->select('folder_id', 'mount_point', 'quota', 'acl', 'storage_id', 'root_id') ->from('group_folders', 'f'); $rows = $query->executeQuery()->fetchAll(); $folderMap = []; foreach ($rows as $row) { - $id = (int)$row['folder_id']; - $folderMap[$id] = [ - 'id' => $id, - 'mount_point' => $row['mount_point'], - 'groups' => $applicableMap[$id] ?? [], - 'quota' => $this->getRealQuota((int)$row['quota']), - 'size' => 0, - 'acl' => (bool)$row['acl'] - ]; + $folder = $this->rowToFolder($row); + $id = $folder->id; + $folderMap[$id] = FolderDefinitionWithMappings::fromFolder( + $folder, + $applicableMap[$id] ?? [], + $this->getManageAcl($folderMappings[$id] ?? []), + ); } return $folderMap; } - /** - * @throws Exception - */ - private function getGroupFolderRootId(int $rootStorageId): int { - $query = $this->connection->getQueryBuilder(); - - $query->select('fileid') - ->from('filecache') - ->where($query->expr()->eq('storage', $query->createNamedParameter($rootStorageId))) - ->andWhere($query->expr()->eq('path_hash', $query->createNamedParameter(md5('__groupfolders')))); - - return (int)$query->executeQuery()->fetchOne(); - } + private function selectWithFileCache(?IQueryBuilder $query = null): IQueryBuilder { + if (!$query) { + $query = $this->connection->getQueryBuilder(); + } - private function joinQueryWithFileCache(IQueryBuilder $query, int $rootStorageId): void { - $query->leftJoin('f', 'filecache', 'c', $query->expr()->andX( - $query->expr()->eq('c.name', $query->expr()->castColumn('f.folder_id', IQueryBuilder::PARAM_STR)), - $query->expr()->eq('c.parent', $query->createNamedParameter($this->getGroupFolderRootId($rootStorageId))), - )); - $query->hintShardKey('c.storage', $rootStorageId); + $query->select( + 'f.folder_id', + 'mount_point', + 'quota', + 'acl', + 'storage_id', + 'root_id', + 'c.fileid', + 'c.storage', + 'c.path', + 'c.name', + 'c.mimetype', + 'c.mimepart', + 'c.size', + 'c.mtime', + 'c.storage_mtime', + 'c.etag', + 'c.encrypted', + 'c.parent', + ) + ->selectAlias('c.permissions', 'permissions') + ->from('group_folders', 'f') + ->leftJoin('f', 'filecache', 'c', $query->expr()->eq('c.fileid', 'f.root_id')); + return $query; } /** - * @return array + * @return array * @throws Exception */ - public function getAllFoldersWithSize(int $rootStorageId): array { + public function getAllFoldersWithSize(): array { $applicableMap = $this->getAllApplicable(); - $query = $this->connection->getQueryBuilder(); - - $query->select('folder_id', 'mount_point', 'quota', 'c.size', 'acl') - ->from('group_folders', 'f'); - $this->joinQueryWithFileCache($query, $rootStorageId); + $query = $this->selectWithFileCache(); $rows = $query->executeQuery()->fetchAll(); @@ -154,42 +142,38 @@ public function getAllFoldersWithSize(int $rootStorageId): array { $folderMap = []; foreach ($rows as $row) { - $id = (int)$row['folder_id']; - $mappings = $folderMappings[$id] ?? []; - $folderMap[$id] = [ - 'id' => $id, - 'mount_point' => $row['mount_point'], - 'groups' => $applicableMap[$id] ?? [], - 'quota' => $this->getRealQuota((int)$row['quota']), - 'size' => $row['size'] ? (int)$row['size'] : 0, - 'acl' => (bool)$row['acl'], - 'manage' => $this->getManageAcl($mappings) - ]; + $folder = $this->rowToFolder($row); + $id = $folder->id; + $folderMap[$id] = FolderWithMappingsAndCache::fromFolderWithMapping( + FolderDefinitionWithMappings::fromFolder( + $folder, + $applicableMap[$id] ?? [], + $this->getManageAcl($folderMappings[$id] ?? []), + ), + Cache::cacheEntryFromData($row, $this->mimeTypeLoader), + ); } return $folderMap; } /** - * @return array + * @return array * @throws Exception */ - public function getAllFoldersForUserWithSize(int $rootStorageId, IUser $user): array { + public function getAllFoldersForUserWithSize(IUser $user): array { $groups = $this->groupManager->getUserGroupIds($user); $applicableMap = $this->getAllApplicable(); - $query = $this->connection->getQueryBuilder(); - - $query->select('f.folder_id', 'mount_point', 'quota', 'c.size', 'acl') - ->from('group_folders', 'f') - ->innerJoin( - 'f', - 'group_folders_groups', - 'a', - $query->expr()->eq('f.folder_id', 'a.folder_id') - ) + $query = $this->selectWithFileCache(); + $query->innerJoin( + 'f', + 'group_folders_groups', + 'a', + $query->expr()->eq('f.folder_id', 'a.folder_id'), + ) + ->selectAlias('a.permissions', 'group_permissions') ->where($query->expr()->in('a.group_id', $query->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY))); - $this->joinQueryWithFileCache($query, $rootStorageId); $rows = $query->executeQuery()->fetchAll(); @@ -197,17 +181,16 @@ public function getAllFoldersForUserWithSize(int $rootStorageId, IUser $user): a $folderMap = []; foreach ($rows as $row) { - $id = (int)$row['folder_id']; - $mappings = $folderMappings[$id] ?? []; - $folderMap[$id] = [ - 'id' => $id, - 'mount_point' => $row['mount_point'], - 'groups' => $applicableMap[$id] ?? [], - 'quota' => $this->getRealQuota((int)$row['quota']), - 'size' => $row['size'] ? (int)$row['size'] : 0, - 'acl' => (bool)$row['acl'], - 'manage' => $this->getManageAcl($mappings) - ]; + $folder = $this->rowToFolder($row); + $id = $folder->id; + $folderMap[$id] = FolderWithMappingsAndCache::fromFolderWithMapping( + FolderDefinitionWithMappings::fromFolder( + $folder, + $applicableMap[$id] ?? [], + $this->getManageAcl($folderMappings[$id] ?? []), + ), + Cache::cacheEntryFromData($row, $this->mimeTypeLoader), + ); } return $folderMap; @@ -267,7 +250,7 @@ private function getManageAcl(array $mappings): array { return [ 'type' => 'user', 'id' => (string)$user->getUID(), - 'displayname' => (string)$user->getDisplayName() + 'displayname' => (string)$user->getDisplayName(), ]; } @@ -280,7 +263,7 @@ private function getManageAcl(array $mappings): array { return [ 'type' => 'group', 'id' => $group->getGID(), - 'displayname' => $group->getDisplayName() + 'displayname' => $group->getDisplayName(), ]; } @@ -293,7 +276,7 @@ private function getManageAcl(array $mappings): array { return [ 'type' => 'circle', 'id' => $circle->getSingleId(), - 'displayname' => $circle->getDisplayName() + 'displayname' => $circle->getDisplayName(), ]; } @@ -301,19 +284,12 @@ private function getManageAcl(array $mappings): array { }, $mappings))); } - /** - * @return ?InternalFolderOut - * @throws Exception - */ - public function getFolder(int $id, int $rootStorageId = 0): ?array { + public function getFolder(int $id): ?FolderWithMappingsAndCache { $applicableMap = $this->getAllApplicable(); - $query = $this->connection->getQueryBuilder(); + $query = $this->selectWithFileCache(); - $query->select('folder_id', 'mount_point', 'quota', 'c.size', 'acl') - ->from('group_folders', 'f') - ->where($query->expr()->eq('folder_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))); - $this->joinQueryWithFileCache($query, $rootStorageId); + $query->where($query->expr()->eq('f.folder_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))); $result = $query->executeQuery(); $row = $result->fetch(); @@ -324,15 +300,16 @@ public function getFolder(int $id, int $rootStorageId = 0): ?array { $folderMappings = $this->getFolderMappings($id); - return [ - 'id' => $id, - 'mount_point' => (string)$row['mount_point'], - 'groups' => $applicableMap[$id] ?? [], - 'quota' => $this->getRealQuota((int)$row['quota']), - 'size' => $row['size'] ?: 0, - 'acl' => (bool)$row['acl'], - 'manage' => $this->getManageAcl($folderMappings) - ]; + $folder = $this->rowToFolder($row); + $id = $folder->id; + return FolderWithMappingsAndCache::fromFolderWithMapping( + FolderDefinitionWithMappings::fromFolder( + $folder, + $applicableMap[$id] ?? [], + $this->getManageAcl($folderMappings), + ), + Cache::cacheEntryFromData($row, $this->mimeTypeLoader), + ); } /** @@ -388,7 +365,7 @@ private function getAllApplicable(): array { $entry = [ 'displayName' => $row['group_id'], 'permissions' => (int)$row['permissions'], - 'type' => 'group' + 'type' => 'group', ]; } else { $entityId = (string)$row['circle_id']; @@ -401,7 +378,7 @@ private function getAllApplicable(): array { $entry = [ 'displayName' => $circle?->getDisplayName() ?? $row['circle_id'], 'permissions' => (int)$row['permissions'], - 'type' => 'circle' + 'type' => 'circle', ]; } @@ -412,8 +389,8 @@ private function getAllApplicable(): array { } /** - * @throws Exception * @return list + * @throws Exception */ private function getGroups(int $id): array { $groups = $this->getAllApplicable()[$id] ?? []; @@ -421,13 +398,13 @@ private function getGroups(int $id): array { return array_map(fn (IGroup $group): array => [ 'gid' => $group->getGID(), - 'displayname' => $group->getDisplayName() + 'displayname' => $group->getDisplayName(), ], array_values(array_filter($groups))); } /** - * @throws Exception * @return list + * @throws Exception */ private function getCircles(int $id): array { $circles = $this->getAllApplicable()[$id] ?? []; @@ -451,7 +428,7 @@ private function getCircles(int $id): array { return array_map(fn (Circle $circle): array => [ 'sid' => $circle->getSingleId(), - 'displayname' => $circle->getDisplayName() + 'displayname' => $circle->getDisplayName(), ], array_values(array_filter(array_merge($circles, $nested)))); } @@ -459,6 +436,7 @@ private function getCircles(int $id): array { * Check if the user is able to configure the advanced folder permissions. This * is the case if the user is an admin, has admin permissions for the group folder * app or is member of a group that can manage permissions for the specific folder. + * * @throws Exception */ public function canManageACL(int $folderId, IUser $user): bool { @@ -499,8 +477,8 @@ private function getManagerMappings(int $folderId): array { } /** - * @throws Exception * @return list + * @throws Exception */ public function searchGroups(int $id, string $search = ''): array { $groups = $this->getGroups($id); @@ -512,8 +490,8 @@ public function searchGroups(int $id, string $search = ''): array { } /** - * @throws Exception * @return list + * @throws Exception */ public function searchCircles(int $id, string $search = ''): array { $circles = $this->getCircles($id); @@ -525,8 +503,8 @@ public function searchCircles(int $id, string $search = ''): array { } /** - * @throws Exception * @return list + * @throws Exception */ public function searchUsers(int $id, string $search = '', int $limit = 10, int $offset = 0): array { $groups = $this->getGroups($id); @@ -539,7 +517,7 @@ public function searchUsers(int $id, string $search = '', int $limit = 10, int $ if (!isset($users[$uid])) { $users[$uid] = [ 'uid' => $uid, - 'displayname' => $displayName + 'displayname' => $displayName, ]; } } @@ -561,7 +539,7 @@ public function searchUsers(int $id, string $search = '', int $limit = 10, int $ if (!isset($users[$uid])) { $users[$uid] = [ 'uid' => $uid, - 'displayname' => $member->getDisplayName() + 'displayname' => $member->getDisplayName(), ]; } } @@ -570,92 +548,62 @@ public function searchUsers(int $id, string $search = '', int $limit = 10, int $ return array_values($users); } + private function rowToFolder(array $row): FolderDefinition { + return new FolderDefinition( + (int)$row['folder_id'], + (string)$row['mount_point'], + $this->getRealQuota((int)$row['quota']), + (bool)$row['acl'], + (int)$row['storage_id'], + (int)$row['root_id'], + ); + } + /** - * @return list + * @return list * @throws Exception */ - public function getFoldersForGroup(string $groupId, int $rootStorageId = 0): array { - $query = $this->connection->getQueryBuilder(); + public function getFoldersForGroup(string $groupId): array { + $query = $this->selectWithFileCache(); - $query->select( - 'f.folder_id', - 'mount_point', - 'quota', - 'acl', - 'c.fileid', - 'c.storage', - 'c.path', - 'c.name', - 'c.mimetype', - 'c.mimepart', - 'c.size', - 'c.mtime', - 'c.storage_mtime', - 'c.etag', - 'c.encrypted', - 'c.parent' + $query->innerJoin( + 'f', + 'group_folders_groups', + 'a', + $query->expr()->eq('f.folder_id', 'a.folder_id'), ) ->selectAlias('a.permissions', 'group_permissions') - ->selectAlias('c.permissions', 'permissions') - ->from('group_folders', 'f') - ->innerJoin( - 'f', - 'group_folders_groups', - 'a', - $query->expr()->eq('f.folder_id', 'a.folder_id') - ) ->where($query->expr()->eq('a.group_id', $query->createNamedParameter($groupId))); - $this->joinQueryWithFileCache($query, $rootStorageId); - - $result = $query->executeQuery()->fetchAll(); - - return array_values(array_map(fn (array $folder): array => [ - 'folder_id' => (int)$folder['folder_id'], - 'mount_point' => (string)$folder['mount_point'], - 'permissions' => (int)$folder['group_permissions'], - 'quota' => $this->getRealQuota((int)$folder['quota']), - 'acl' => (bool)$folder['acl'], - 'rootCacheEntry' => (isset($folder['fileid'])) ? Cache::cacheEntryFromData($folder, $this->mimeTypeLoader) : null - ], $result)); + + return array_values(array_map(function (array $row): FolderDefinitionWithPermissions { + $folder = $this->rowToFolder($row); + return FolderDefinitionWithPermissions::fromFolder( + $folder, + Cache::cacheEntryFromData($row, $this->mimeTypeLoader), + (int)$row['group_permissions'] + ); + }, $query->executeQuery()->fetchAll())); } /** * @param string[] $groupIds - * @return list + * @return list * @throws Exception */ - public function getFoldersForGroups(array $groupIds, int $rootStorageId = 0): array { - $query = $this->connection->getQueryBuilder(); + public function getFoldersForGroups(array $groupIds): array { + if (count($groupIds) === 0) { + return []; + } + $query = $this->selectWithFileCache(); - $query->select( - 'f.folder_id', - 'mount_point', - 'quota', - 'acl', - 'c.fileid', - 'c.storage', - 'c.path', - 'c.name', - 'c.mimetype', - 'c.mimepart', - 'c.size', - 'c.mtime', - 'c.storage_mtime', - 'c.etag', - 'c.encrypted', - 'c.parent' + $query->innerJoin( + 'f', + 'group_folders_groups', + 'a', + $query->expr()->eq('f.folder_id', 'a.folder_id'), ) ->selectAlias('a.permissions', 'group_permissions') - ->selectAlias('c.permissions', 'permissions') - ->from('group_folders', 'f') - ->innerJoin( - 'f', - 'group_folders_groups', - 'a', - $query->expr()->eq('f.folder_id', 'a.folder_id') - ) ->where($query->expr()->in('a.group_id', $query->createParameter('groupIds'))); - $this->joinQueryWithFileCache($query, $rootStorageId); // add chunking because Oracle can't deal with more than 1000 values in an expression list for in queries. $result = []; @@ -664,21 +612,21 @@ public function getFoldersForGroups(array $groupIds, int $rootStorageId = 0): ar $result = array_merge($result, $query->executeQuery()->fetchAll()); } - return array_map(fn (array $folder): array => [ - 'folder_id' => (int)$folder['folder_id'], - 'mount_point' => (string)$folder['mount_point'], - 'permissions' => (int)$folder['group_permissions'], - 'quota' => $this->getRealQuota((int)$folder['quota']), - 'acl' => (bool)$folder['acl'], - 'rootCacheEntry' => (isset($folder['fileid'])) ? Cache::cacheEntryFromData($folder, $this->mimeTypeLoader) : null - ], array_values($result)); + return array_values(array_map(function (array $row): FolderDefinitionWithPermissions { + $folder = $this->rowToFolder($row); + return FolderDefinitionWithPermissions::fromFolder( + $folder, + Cache::cacheEntryFromData($row, $this->mimeTypeLoader), + (int)$row['group_permissions'] + ); + }, $result)); } /** - * @return list + * @return list * @throws Exception */ - public function getFoldersFromCircleMemberships(IUser $user, int $rootStorageId = 0): array { + public function getFoldersFromCircleMemberships(IUser $user): array { $circlesManager = $this->getCirclesManager(); if ($circlesManager === null) { return []; @@ -691,35 +639,15 @@ public function getFoldersFromCircleMemberships(IUser $user, int $rootStorageId } $queryHelper = $circlesManager->getQueryHelper(); - $query = $queryHelper->getQueryBuilder(); + $query = $this->selectWithFileCache($queryHelper->getQueryBuilder()); - $query->select( - 'f.folder_id', - 'f.mount_point', - 'f.quota', - 'f.acl', - 'c.fileid', - 'c.storage', - 'c.path', - 'c.name', - 'c.mimetype', - 'c.mimepart', - 'c.size', - 'c.mtime', - 'c.storage_mtime', - 'c.etag', - 'c.encrypted', - 'c.parent' + $query->innerJoin( + 'f', + 'group_folders_groups', + 'a', + $query->expr()->eq('f.folder_id', 'a.folder_id'), ) ->selectAlias('a.permissions', 'group_permissions') - ->selectAlias('c.permissions', 'permissions') - ->from('group_folders', 'f') - ->innerJoin( - 'f', - 'group_folders_groups', - 'a', - $query->expr()->eq('f.folder_id', 'a.folder_id') - ) ->where($query->expr()->neq('a.circle_id', $query->createNamedParameter(''))); /** @psalm-suppress RedundantCondition */ @@ -728,16 +656,15 @@ public function getFoldersFromCircleMemberships(IUser $user, int $rootStorageId } else { $queryHelper->limitToInheritedMembers('a', 'circle_id', $federatedUser); } - $this->joinQueryWithFileCache($query, $rootStorageId); - - return array_map(fn (array $folder): array => [ - 'folder_id' => (int)$folder['folder_id'], - 'mount_point' => (string)$folder['mount_point'], - 'permissions' => (int)$folder['group_permissions'], - 'quota' => $this->getRealQuota((int)$folder['quota']), - 'acl' => (bool)$folder['acl'], - 'rootCacheEntry' => (isset($folder['fileid'])) ? Cache::cacheEntryFromData($folder, $this->mimeTypeLoader) : null - ], array_values($query->executeQuery()->fetchAll())); + + return array_values(array_map(function (array $row): FolderDefinitionWithPermissions { + $folder = $this->rowToFolder($row); + return FolderDefinitionWithPermissions::fromFolder( + $folder, + Cache::cacheEntryFromData($row, $this->mimeTypeLoader), + $row['group_permissions'] + ); + }, $query->executeQuery()->fetchAll())); } @@ -755,6 +682,13 @@ public function createFolder(string $mountPoint): int { $query->executeStatement(); $id = $query->getLastInsertId(); + ['storage_id' => $storageId, 'root_id' => $rootId] = $this->folderStorageManager->getRootAndStorageIdForFolder($id); + $query->update('group_folders') + ->set('root_id', $query->createNamedParameter($rootId)) + ->set('storage_id', $query->createNamedParameter($storageId)) + ->where($query->expr()->eq('folder_id', $query->createNamedParameter($id))); + $query->executeStatement(); + $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('A new groupfolder "%s" was created with id %d', [$mountPoint, $id])); return $id; @@ -776,7 +710,7 @@ public function addApplicableGroup(int $folderId, string $groupId): void { 'folder_id' => $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT), 'group_id' => $query->createNamedParameter($groupId), 'circle_id' => $query->createNamedParameter($circleId ?? ''), - 'permissions' => $query->createNamedParameter(Constants::PERMISSION_ALL) + 'permissions' => $query->createNamedParameter(Constants::PERMISSION_ALL), ]); $query->executeStatement(); @@ -792,14 +726,14 @@ public function removeApplicableGroup(int $folderId, string $groupId): void { $query->delete('group_folders_groups') ->where( $query->expr()->eq( - 'folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT) - ) + 'folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT), + ), ) ->andWhere( $query->expr()->orX( $query->expr()->eq('group_id', $query->createNamedParameter($groupId)), - $query->expr()->eq('circle_id', $query->createNamedParameter($groupId)) - ) + $query->expr()->eq('circle_id', $query->createNamedParameter($groupId)), + ), ); $query->executeStatement(); @@ -817,14 +751,14 @@ public function setGroupPermissions(int $folderId, string $groupId, int $permiss ->set('permissions', $query->createNamedParameter($permissions, IQueryBuilder::PARAM_INT)) ->where( $query->expr()->eq( - 'folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT) - ) + 'folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT), + ), ) ->andWhere( $query->expr()->orX( $query->expr()->eq('group_id', $query->createNamedParameter($groupId)), - $query->expr()->eq('circle_id', $query->createNamedParameter($groupId)) - ) + $query->expr()->eq('circle_id', $query->createNamedParameter($groupId)), + ), ); $query->executeStatement(); @@ -842,7 +776,7 @@ public function setManageACL(int $folderId, string $type, string $id, bool $mana ->values([ 'folder_id' => $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT), 'mapping_type' => $query->createNamedParameter($type), - 'mapping_id' => $query->createNamedParameter($id) + 'mapping_id' => $query->createNamedParameter($id), ]); } else { $query->delete('group_folders_manage') @@ -962,21 +896,23 @@ public function setFolderACL(int $folderId, bool $acl): void { } /** - * @return list + * @return list * @throws Exception */ - public function getFoldersForUser(IUser $user, int $rootStorageId = 0): array { + public function getFoldersForUser(IUser $user): array { $groups = $this->groupManager->getUserGroupIds($user); + /** @var list $folders */ $folders = array_merge( - $this->getFoldersForGroups($groups, $rootStorageId), - $this->getFoldersFromCircleMemberships($user, $rootStorageId) + $this->getFoldersForGroups($groups), + $this->getFoldersFromCircleMemberships($user), ); + /** @var array $mergedFolders */ $mergedFolders = []; foreach ($folders as $folder) { - $id = $folder['folder_id']; + $id = $folder->id; if (isset($mergedFolders[$id])) { - $mergedFolders[$id]['permissions'] |= $folder['permissions']; + $mergedFolders[$id] = $mergedFolders[$id]->withAddedPermissions($folder->permissions); } else { $mergedFolders[$id] = $folder; } @@ -990,15 +926,16 @@ public function getFoldersForUser(IUser $user, int $rootStorageId = 0): array { */ public function getFolderPermissionsForUser(IUser $user, int $folderId): int { $groups = $this->groupManager->getUserGroupIds($user); + /** @var list $folders */ $folders = array_merge( $this->getFoldersForGroups($groups), - $this->getFoldersFromCircleMemberships($user) + $this->getFoldersFromCircleMemberships($user), ); $permissions = 0; foreach ($folders as $folder) { - if ($folderId === $folder['folder_id']) { - $permissions |= $folder['permissions']; + if ($folderId === $folder->id) { + $permissions |= $folder->permissions; } } diff --git a/lib/Folder/FolderWithMappingsAndCache.php b/lib/Folder/FolderWithMappingsAndCache.php new file mode 100644 index 000000000..4ddf3d4fe --- /dev/null +++ b/lib/Folder/FolderWithMappingsAndCache.php @@ -0,0 +1,68 @@ + $groups + * @psalm-param list $manage + */ + public function __construct( + int $id, + string $mountPoint, + int $quota, + bool $acl, + int $storageId, + int $rootId, + array $groups, + array $manage, + public readonly ICacheEntry $rootCacheEntry, + ) { + parent::__construct($id, $mountPoint, $quota, $acl, $storageId, $rootId, $groups, $manage); + } + + /** + * @psalm-param array $groups + * @psalm-param list $manage + */ + public static function fromFolderWithMapping(FolderDefinitionWithMappings $folder, ICacheEntry $rootCacheEntry): FolderWithMappingsAndCache { + return new FolderWithMappingsAndCache( + $folder->id, + $folder->mountPoint, + $folder->quota, + $folder->acl, + $folder->storageId, + $folder->rootId, + $folder->groups, + $folder->manage, + $rootCacheEntry, + ); + } + + public function toArray(): array { + return [ + 'id' => $this->id, + 'mount_point' => $this->mountPoint, + 'quota' => $this->quota, + 'acl' => $this->acl, + 'storage_id' => $this->storageId, + 'root_id' => $this->rootId, + 'root_cache_entry' => $this->rootCacheEntry, + 'groups' => $this->groups, + 'manage' => $this->manage, + ]; + } +} diff --git a/lib/Migration/Version102020Date20180806161449.php b/lib/Migration/Version102020Date20180806161449.php index 9881f71c4..0b9933505 100644 --- a/lib/Migration/Version102020Date20180806161449.php +++ b/lib/Migration/Version102020Date20180806161449.php @@ -9,6 +9,7 @@ namespace OCA\GroupFolders\Migration; use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; @@ -34,6 +35,12 @@ public function changeSchema(IOutput $output, \Closure $schemaClosure, array $op // Removed in migration Version19000Date20240903062631 //'default' => -3, ]); + + // from Version20000Date20250612140256.php + $table->addColumn('root_id', Types::BIGINT, ['notnull' => false]); + $table->addColumn('storage_id', Types::BIGINT, ['notnull' => false]); + $table->addColumn('options', Types::TEXT, ['notnull' => false]); + $table->setPrimaryKey(['folder_id']); } diff --git a/lib/Migration/Version20000Date20250612140256.php b/lib/Migration/Version20000Date20250612140256.php new file mode 100644 index 000000000..550101ab7 --- /dev/null +++ b/lib/Migration/Version20000Date20250612140256.php @@ -0,0 +1,131 @@ +hasTable('group_folders')) { + $table = $schema->getTable('group_folders'); + if (!$table->hasColumn('root_id')) { + $table->addColumn('root_id', Types::BIGINT, ['notnull' => false]); + } + if (!$table->hasColumn('storage_id')) { + $table->addColumn('storage_id', Types::BIGINT, ['notnull' => false]); + } + if (!$table->hasColumn('options')) { + $table->addColumn('options', Types::TEXT, ['notnull' => false]); + } + return $schema; + } + return null; + } + + #[Override] + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $rootIds = $this->getJailedRootIds(); + $storageId = $this->getJailedGroupFolderStorageId(); + if (count($rootIds) === 0 || $storageId === null) { + return; + } + + try { + $this->connection->beginTransaction(); + + $query = $this->connection->getQueryBuilder(); + $query->update('group_folders') + ->set('root_id', $query->createParameter('root_id')) + ->set('storage_id', $query->createNamedParameter($storageId)) + ->where($query->expr()->eq('folder_id', $query->createParameter('folder_id'))); + + foreach ($rootIds as $folderId => $rootId) { + $query->setParameter('root_id', $rootId); + $query->setParameter('folder_id', $folderId); + $query->executeStatement(); + } + + $this->connection->commit(); + } catch (\Exception $e) { + $this->connection->rollBack(); + throw $e; + } + } + + /** + * @return array + */ + private function getJailedRootIds(): array { + $parentFolderId = $this->getJailedGroupFolderRootId(); + if ($parentFolderId === null) { + return []; + } + + $query = $this->connection->getQueryBuilder(); + $query->select('name', 'fileid') + ->from('filecache') + ->where($query->expr()->eq('parent', $query->createNamedParameter($parentFolderId))); + $result = $query->executeQuery(); + + $rootIds = []; + while ($row = $result->fetch()) { + if (is_numeric($row['name'])) { + $rootIds[(int)$row['name']] = (int)$row['fileid']; + } + } + return $rootIds; + } + + private function getJailedGroupFolderRootId(): ?int { + $query = $this->connection->getQueryBuilder(); + $query->select('fileid') + ->from('filecache') + ->andWhere($query->expr()->eq('path_hash', $query->createNamedParameter(md5('__groupfolders')))); + + $id = $query->executeQuery()->fetchOne(); + if ($id === null) { + return null; + } else { + return (int)$id; + } + } + + private function getJailedGroupFolderStorageId(): ?int { + $query = $this->connection->getQueryBuilder(); + $query->select('storage') + ->from('filecache') + ->andWhere($query->expr()->eq('path_hash', $query->createNamedParameter(md5('__groupfolders')))); + + $id = $query->executeQuery()->fetchOne(); + if ($id === null) { + return null; + } else { + return (int)$id; + } + } +} diff --git a/lib/Migration/WrongDefaultQuotaRepairStep.php b/lib/Migration/WrongDefaultQuotaRepairStep.php index 60eb7487a..1048eef63 100644 --- a/lib/Migration/WrongDefaultQuotaRepairStep.php +++ b/lib/Migration/WrongDefaultQuotaRepairStep.php @@ -30,7 +30,7 @@ public function getName(): string { */ public function run(IOutput $output): void { foreach ($this->manager->getAllFolders() as $id => $folder) { - $quota = $folder['quota']; + $quota = $folder->quota; $changed = false; if ($quota === 1073741274) { diff --git a/lib/Mount/FolderStorageManager.php b/lib/Mount/FolderStorageManager.php new file mode 100644 index 000000000..fd1768a28 --- /dev/null +++ b/lib/Mount/FolderStorageManager.php @@ -0,0 +1,104 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\GroupFolders\Mount; + +use OC\Files\Storage\Wrapper\Jail; +use OCA\GroupFolders\ACL\ACLManagerFactory; +use OCA\GroupFolders\ACL\ACLStorageWrapper; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\Files\Storage\IStorage; +use OCP\IAppConfig; +use OCP\IUser; + +class FolderStorageManager { + private readonly bool $enableEncryption; + + public function __construct( + private readonly IRootFolder $rootFolder, + private readonly IAppConfig $appConfig, + private readonly ACLManagerFactory $aclManagerFactory, + ) { + $this->enableEncryption = $this->appConfig->getValueString('groupfolders', 'enable_encryption', 'false') === 'true'; + } + + /** + * @return array{storage_id: int, root_id: int} + */ + public function getRootAndStorageIdForFolder(int $folderId): array { + $storage = $this->getBaseStorageForFolder($folderId); + $cache = $storage->getCache(); + $id = $cache->getId(''); + if ($id === -1) { + $storage->getScanner()->scan(''); + $id = $cache->getId(''); + if ($id === -1) { + throw new \Exception('Group folder root is not in cache even after scanning for folder ' . $folderId); + } + } + return [ + 'storage_id' => $cache->getNumericStorageId(), + 'root_id' => $id, + ]; + } + + /** + * @param 'files'|'trash'|'versions' $type + */ + public function getBaseStorageForFolder(int $folderId, ?\OCA\GroupFolders\Folder\FolderDefinition $folder = null, ?IUser $user = null, bool $inShare = false, string $type = 'files'): IStorage { + try { + /** @var Folder $parentFolder */ + $parentFolder = $this->rootFolder->get('__groupfolders'); + } catch (NotFoundException) { + $parentFolder = $this->rootFolder->newFolder('__groupfolders'); + } + + if ($type !== 'files') { + try { + /** @var Folder $parentFolder */ + $parentFolder = $parentFolder->get($type); + } catch (NotFoundException) { + $parentFolder = $parentFolder->newFolder($type); + } + } + + try { + /** @var Folder $storageFolder */ + $storageFolder = $parentFolder->get((string)$folderId); + } catch (NotFoundException) { + $storageFolder = $parentFolder->newFolder((string)$folderId); + } + $rootStorage = $storageFolder->getStorage(); + $rootPath = $storageFolder->getInternalPath(); + + // apply acl before jail + if ($folder && $folder->acl && $user) { + $aclManager = $this->aclManagerFactory->getACLManager($user); + $rootStorage = new ACLStorageWrapper([ + 'storage' => $rootStorage, + 'acl_manager' => $aclManager, + 'in_share' => $inShare, + 'storage_id' => $rootStorage->getCache()->getNumericStorageId(), + ]); + } + + if ($this->enableEncryption) { + return new GroupFolderEncryptionJail([ + 'storage' => $rootStorage, + 'root' => $rootPath, + ]); + } else { + return new Jail([ + 'storage' => $rootStorage, + 'root' => $rootPath, + ]); + } + } +} diff --git a/lib/Mount/GroupFolderStorage.php b/lib/Mount/GroupFolderStorage.php index cbbc2800c..694e25201 100644 --- a/lib/Mount/GroupFolderStorage.php +++ b/lib/Mount/GroupFolderStorage.php @@ -12,6 +12,7 @@ use OC\Files\ObjectStore\ObjectStoreScanner; use OC\Files\ObjectStore\ObjectStoreStorage; use OC\Files\Storage\Wrapper\Quota; +use OCA\GroupFolders\Folder\FolderDefinition; use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; use OCP\Files\Cache\IScanner; @@ -21,7 +22,7 @@ use OCP\IUserSession; class GroupFolderStorage extends Quota implements IConstructableStorage { - private readonly int $folderId; + private readonly FolderDefinition $folder; private readonly ?ICacheEntry $rootEntry; private readonly IUserSession $userSession; private readonly ?IUser $mountOwner; @@ -30,14 +31,18 @@ class GroupFolderStorage extends Quota implements IConstructableStorage { public function __construct($parameters) { parent::__construct($parameters); - $this->folderId = $parameters['folder_id']; + $this->folder = $parameters['folder']; $this->rootEntry = $parameters['rootCacheEntry']; $this->userSession = $parameters['userSession']; $this->mountOwner = $parameters['mountOwner']; } public function getFolderId(): int { - return $this->folderId; + return $this->folder->id; + } + + public function getFolder(): FolderDefinition { + return $this->folder; } public function getOwner(string $path): string|false { diff --git a/lib/Mount/MountProvider.php b/lib/Mount/MountProvider.php index 2008669a9..ee3e55d55 100644 --- a/lib/Mount/MountProvider.php +++ b/lib/Mount/MountProvider.php @@ -8,11 +8,10 @@ namespace OCA\GroupFolders\Mount; -use OC\Files\Storage\Wrapper\Jail; use OC\Files\Storage\Wrapper\PermissionsMask; use OCA\GroupFolders\ACL\ACLManager; use OCA\GroupFolders\ACL\ACLManagerFactory; -use OCA\GroupFolders\ACL\ACLStorageWrapper; +use OCA\GroupFolders\Folder\FolderDefinitionWithPermissions; use OCA\GroupFolders\Folder\FolderManager; use OCP\Constants; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -25,18 +24,13 @@ use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorage; use OCP\Files\Storage\IStorageFactory; -use OCP\ICache; use OCP\IDBConnection; use OCP\IRequest; use OCP\IUser; use OCP\IUserSession; -/** - * @psalm-import-type InternalFolder from FolderManager - */ class MountProvider implements IMountProvider { private ?Folder $root = null; - private ?int $rootStorageId = null; public function __construct( private readonly FolderManager $folderManager, @@ -46,54 +40,40 @@ public function __construct( private readonly IRequest $request, private readonly IMountProviderCollection $mountProviderCollection, private readonly IDBConnection $connection, - private readonly ICache $cache, + private readonly FolderStorageManager $folderStorageManager, private readonly bool $allowRootShare, private readonly bool $enableEncryption, ) { } - private function getRootStorageId(): int { - if ($this->rootStorageId === null) { - $cached = $this->cache->get('root_storage_id'); - if ($cached !== null) { - $this->rootStorageId = $cached; - } else { - $id = $this->getRootFolder()->getStorage()->getCache()->getNumericStorageId(); - $this->cache->set('root_storage_id', $id); - $this->rootStorageId = $id; - } - } - - return $this->rootStorageId; - } - /** - * @return list + * @return list */ public function getFoldersForUser(IUser $user): array { - return $this->folderManager->getFoldersForUser($user, $this->getRootStorageId()); + return $this->folderManager->getFoldersForUser($user); } public function getMountsForUser(IUser $user, IStorageFactory $loader): array { $folders = $this->getFoldersForUser($user); - $mountPoints = array_map(fn (array $folder): string => 'files/' . $folder['mount_point'], $folders); + $mountPoints = array_map(fn (FolderDefinitionWithPermissions $folder): string => 'files/' . $folder->mountPoint, $folders); $conflicts = $this->findConflictsForUser($user, $mountPoints); - $foldersWithAcl = array_filter($folders, fn (array $folder): bool => $folder['acl']); - $aclRootPaths = array_map(fn (array $folder): string => $this->getJailPath($folder['folder_id']), $foldersWithAcl); - $aclManager = $this->aclManagerFactory->getACLManager($user, $this->getRootStorageId()); - $rootRules = $aclManager->getRelevantRulesForPath($aclRootPaths); + /** @var array $foldersWithAcl */ + $foldersWithAcl = array_filter($folders, fn (FolderDefinitionWithPermissions $folder): bool => $folder->acl); + $rootFileIds = array_map(fn (FolderDefinitionWithPermissions $folder): int => $folder->rootId, $foldersWithAcl); + $aclManager = $this->aclManagerFactory->getACLManager($user); + $rootRules = $aclManager->getRulesByFileIds($rootFileIds); - return array_filter(array_map(function (array $folder) use ($user, $loader, $conflicts, $aclManager, $rootRules): ?IMountPoint { + return array_filter(array_map(function (FolderDefinitionWithPermissions $folder) use ($user, $loader, $conflicts, $aclManager, $rootRules): ?IMountPoint { // check for existing files in the user home and rename them if needed - $originalFolderName = $folder['mount_point']; + $originalFolderName = $folder->mountPoint; if (in_array($originalFolderName, $conflicts)) { /** @var IStorage $userStorage */ $userStorage = $this->mountProviderCollection->getHomeMountForUser($user)->getStorage(); $userCache = $userStorage->getCache(); $i = 1; - $folderName = $folder['mount_point'] . ' (' . $i++ . ')'; + $folderName = $folder->mountPoint . ' (' . $i++ . ')'; while ($userCache->inCache("files/$folderName")) { $folderName = $originalFolderName . ' (' . $i++ . ')'; @@ -105,13 +85,9 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader): array { } return $this->getMount( - $folder['folder_id'], - '/' . $user->getUID() . '/files/' . $folder['mount_point'], - $folder['permissions'], - $folder['quota'], - $folder['rootCacheEntry'], + $folder, + '/' . $user->getUID() . '/files/' . $folder->mountPoint, $loader, - $folder['acl'], $user, $aclManager, $rootRules @@ -139,54 +115,31 @@ private function getCurrentUID(): ?string { } public function getMount( - int $id, + FolderDefinitionWithPermissions $folder, string $mountPoint, - int $permissions, - int $quota, - ?ICacheEntry $cacheEntry = null, ?IStorageFactory $loader = null, - bool $acl = false, ?IUser $user = null, ?ACLManager $aclManager = null, array $rootRules = [], ): ?IMountPoint { - if (!$cacheEntry) { - // trigger folder creation - $folder = $this->getFolder($id); - if ($folder === null) { - return null; - } - - $cacheEntry = $this->getRootFolder()->getStorage()->getCache()->get($folder->getId()); - if ($cacheEntry === false) { - return null; - } - } + $cacheEntry = $folder->rootCacheEntry; $storage = $this->getRootFolder()->getStorage(); $storage->setOwner($user?->getUID()); - $rootPath = $this->getJailPath($id); + $rootPath = $this->getJailPath($folder->id); - // apply acl before jail - if ($acl && $user) { - $inShare = !\OC::$CLI && ($this->getCurrentUID() === null || $this->getCurrentUID() !== $user->getUID()); - $aclManager ??= $this->aclManagerFactory->getACLManager($user, $this->getRootStorageId()); + if ($aclManager && $folder->acl && $user) { $aclRootPermissions = $aclManager->getPermissionsForPathFromRules($rootPath, $rootRules); - $storage = new ACLStorageWrapper([ - 'storage' => $storage, - 'acl_manager' => $aclManager, - 'in_share' => $inShare, - ]); $cacheEntry['permissions'] &= $aclRootPermissions; } - $quotaStorage = $this->getGroupFolderStorage($id, $storage, $user, $rootPath, $quota, $cacheEntry); + $quotaStorage = $this->getGroupFolderStorage($folder, $user, $cacheEntry); $maskedStore = new PermissionsMask([ 'storage' => $quotaStorage, - 'mask' => $permissions, + 'mask' => $folder->permissions, ]); if (!$this->allowRootShare) { @@ -197,7 +150,7 @@ public function getMount( } return new GroupMountPoint( - $id, + $folder->id, $maskedStore, $mountPoint, null, @@ -206,7 +159,7 @@ public function getMount( } public function getTrashMount( - int $id, + FolderDefinitionWithPermissions $folder, string $mountPoint, int $quota, IStorageFactory $loader, @@ -218,12 +171,10 @@ public function getTrashMount( $storage->setOwner($user->getUID()); - $trashPath = $this->getRootFolder()->getInternalPath() . '/trash/' . $id; - - $trashStorage = $this->getGroupFolderStorage($id, $storage, $user, $trashPath, $quota, $cacheEntry); + $trashStorage = $this->getGroupFolderStorage($folder, $user, $cacheEntry, 'trash'); return new GroupMountPoint( - $id, + $folder->id, $trashStorage, $mountPoint, null, @@ -231,36 +182,35 @@ public function getTrashMount( ); } + /** + * @param 'files'|'trash'|'versions' $type + */ public function getGroupFolderStorage( - int $id, - IStorage $rootStorage, + FolderDefinitionWithPermissions $folder, ?IUser $user, - string $rootPath, - int $quota, ?ICacheEntry $rootCacheEntry, + string $type = 'files', ): IStorage { + if ($user) { + $inShare = !\OC::$CLI && ($this->getCurrentUID() === null || $this->getCurrentUID() !== $user->getUID()); + $baseStorage = $this->folderStorageManager->getBaseStorageForFolder($folder->id, $folder, $user, $inShare, $type); + } else { + $baseStorage = $this->folderStorageManager->getBaseStorageForFolder($folder->id, $folder, null, false, $type); + } if ($this->enableEncryption) { - $baseStorage = new GroupFolderEncryptionJail([ - 'storage' => $rootStorage, - 'root' => $rootPath, - ]); $quotaStorage = new GroupFolderStorage([ 'storage' => $baseStorage, - 'quota' => $quota, - 'folder_id' => $id, + 'quota' => $folder->quota, + 'folder' => $folder, 'rootCacheEntry' => $rootCacheEntry, 'userSession' => $this->userSession, 'mountOwner' => $user, ]); } else { - $baseStorage = new Jail([ - 'storage' => $rootStorage, - 'root' => $rootPath, - ]); $quotaStorage = new GroupFolderNoEncryptionStorage([ 'storage' => $baseStorage, - 'quota' => $quota, - 'folder_id' => $id, + 'quota' => $folder->quota, + 'folder' => $folder, 'rootCacheEntry' => $rootCacheEntry, 'userSession' => $this->userSession, 'mountOwner' => $user, diff --git a/lib/Trash/GroupTrashItem.php b/lib/Trash/GroupTrashItem.php index b18cd4c13..799c6cee8 100644 --- a/lib/Trash/GroupTrashItem.php +++ b/lib/Trash/GroupTrashItem.php @@ -10,6 +10,7 @@ use OCA\Files_Trashbin\Trash\ITrashBackend; use OCA\Files_Trashbin\Trash\TrashItem; +use OCA\GroupFolders\Folder\FolderDefinition; use OCP\Files\FileInfo; use OCP\IUser; @@ -23,6 +24,7 @@ public function __construct( IUser $user, private readonly string $mountPoint, ?IUser $deletedBy, + public readonly FolderDefinition $folder, ) { parent::__construct($backend, $this->mountPoint . '/' . $this->internalOriginalLocation, $deletedTime, $trashPath, $fileInfo, $user, $deletedBy); } @@ -54,4 +56,8 @@ public function getInternalPath(): string { return rtrim($path, '.d' . $this->getDeletedTime()); } + + public function getGroupFolderStorageId(): int { + return $this->folder->storageId; + } } diff --git a/lib/Trash/TrashBackend.php b/lib/Trash/TrashBackend.php index 55cc976ab..49607b911 100644 --- a/lib/Trash/TrashBackend.php +++ b/lib/Trash/TrashBackend.php @@ -16,7 +16,9 @@ use OCA\Files_Trashbin\Trash\ITrashBackend; use OCA\Files_Trashbin\Trash\ITrashItem; use OCA\GroupFolders\ACL\ACLManagerFactory; +use OCA\GroupFolders\Folder\FolderDefinitionWithPermissions; use OCA\GroupFolders\Folder\FolderManager; +use OCA\GroupFolders\Folder\FolderWithMappingsAndCache; use OCA\GroupFolders\Mount\GroupFolderStorage; use OCA\GroupFolders\Mount\MountProvider; use OCA\GroupFolders\Versions\VersionsBackend; @@ -36,9 +38,6 @@ use OCP\IUserSession; use Psr\Log\LoggerInterface; -/** - * @psalm-import-type InternalFolder from FolderManager - */ class TrashBackend implements ITrashBackend { private ?VersionsBackend $versionsBackend = null; @@ -84,10 +83,10 @@ public function listTrashFolder(ITrashItem $folder): array { } $content = $folderNode->getDirectoryListing(); - $this->aclManagerFactory->getACLManager($user)->preloadRulesForFolder($folder->getPath()); + $this->aclManagerFactory->getACLManager($user)->preloadRulesForFolder($folder->getGroupFolderStorageId(), $folder->getPath()); return array_values(array_filter(array_map(function (Node $node) use ($folder, $user): ?GroupTrashItem { - if (!$this->userHasAccessToPath($user, $this->getUnJailedPath($node))) { + if (!$this->userHasAccessToPath($folder->getGroupFolderStorageId(), $user, $this->getUnJailedPath($node))) { return null; } @@ -100,6 +99,7 @@ public function listTrashFolder(ITrashItem $folder): array { $user, $folder->getGroupFolderMountPoint(), $folder->getDeletedBy(), + $folder->folder, ); }, $content))); } @@ -120,7 +120,7 @@ public function restoreItem(ITrashItem $item): void { throw new NotFoundException(); } - if (!$this->userHasAccessToPath($item->getUser(), $item->getPath(), Constants::PERMISSION_UPDATE)) { + if (!$this->userHasAccessToPath($item->getGroupFolderStorageId(), $item->getUser(), $item->getPath(), Constants::PERMISSION_UPDATE)) { throw new NotPermittedException(); } @@ -221,7 +221,7 @@ public function removeItem(ITrashItem $item): void { throw new NotFoundException(); } - if (!$this->userHasAccessToPath($item->getUser(), $item->getPath(), Constants::PERMISSION_DELETE)) { + if (!$this->userHasAccessToPath($item->folder->storageId, $item->getUser(), $item->getPath(), Constants::PERMISSION_DELETE)) { throw new NotPermittedException(); } @@ -245,12 +245,13 @@ public function moveToTrash(IStorage $storage, string $internalPath): bool { /** @var GroupFolderStorage $storage */ $name = basename($internalPath); $fileEntry = $storage->getCache()->get($internalPath); + $folder = $storage->getFolder(); $folderId = $storage->getFolderId(); $user = $this->userSession->getUser(); $owner = $storage->getUser(); - $this->setupTrashFolder($folderId, $owner); + $this->setupTrashFolder($folder, $owner); $trashFolder = $this->rootFolder->get('/' . $owner->getUID() . '/files_trashbin/groupfolders/' . $folderId); $trashStorage = $trashFolder->getStorage(); @@ -329,19 +330,20 @@ private function moveFromEncryptedStorage(IStorage $sourceStorage, IStorage $tar private function userHasAccessToFolder(IUser $user, int $folderId): bool { $folders = $this->folderManager->getFoldersForUser($user); - $folderIds = array_map(fn (array $folder): int => $folder['folder_id'], $folders); + $folderIds = array_map(fn (array $folder): int => $folder->id, $folders); return in_array($folderId, $folderIds); } private function userHasAccessToPath( + int $storageId, IUser $user, string $path, int $permission = Constants::PERMISSION_READ, ): bool { try { $activePermissions = $this->aclManagerFactory->getACLManager($user) - ->getACLPermissionsForPath($path); + ->getACLPermissionsForPath($storageId, $path); } catch (\Exception $e) { $this->logger->warning("Failed to get permissions for {$path}", ['exception' => $e]); return false; @@ -351,15 +353,19 @@ private function userHasAccessToPath( } private function getNodeForTrashItem(IUser $user, ITrashItem $trashItem): ?Node { + if (!($trashItem instanceof GroupTrashItem)) { + throw new \LogicException('Trying to remove normal trash item in Team folder trash backend'); + } + [, $folderId, $path] = explode('/', $trashItem->getTrashPath(), 3); $folderId = (int)$folderId; $folders = $this->folderManager->getFoldersForUser($user); foreach ($folders as $groupFolder) { - if ($groupFolder['folder_id'] === $folderId) { + if ($groupFolder->id === $folderId) { $trashRoot = $this->rootFolder->get('/' . $user->getUID() . '/files_trashbin/groupfolders/' . $folderId); try { $node = $trashRoot->get($path); - if (!$this->userHasAccessToPath($user, $trashItem->getPath())) { + if (!$this->userHasAccessToPath($trashItem->getGroupFolderStorageId(), $user, $trashItem->getPath())) { return null; } @@ -384,13 +390,14 @@ private function getTrashRoot(): Folder { } } - private function setupTrashFolder(int $folderId, ?IUser $user = null): Folder { + private function setupTrashFolder(FolderDefinitionWithPermissions $folder, ?IUser $user = null): Folder { + $folderId = $folder->id; if ($user) { $mountPoint = '/' . $user->getUID() . '/files_trashbin/groupfolders/' . $folderId; $mount = $this->mountManager->find($mountPoint); if ($mount->getMountPoint() !== $mountPoint) { $trashMount = $this->mountProvider->getTrashMount( - $folderId, + $folder, $mountPoint, FileInfo::SPACE_UNLIMITED, $this->storageFactory, @@ -425,11 +432,11 @@ private function getUnJailedPath(Node $node): string { } /** - * @param list $folders + * @param list $folders * @return list */ private function getTrashForFolders(IUser $user, array $folders): array { - $folderIds = array_map(fn (array $folder): int => $folder['folder_id'], $folders); + $folderIds = array_map(fn (FolderDefinitionWithPermissions $folder): int => $folder->id, $folders); $rows = $this->trashManager->listTrashForFolders($folderIds); $indexedRows = []; $trashItemsByOriginalLocation = []; @@ -441,17 +448,17 @@ private function getTrashForFolders(IUser $user, array $folders): array { $items = []; foreach ($folders as $folder) { - $folderId = $folder['folder_id']; - $folderHasAcl = $folder['acl']; - $mountPoint = $folder['mount_point']; + $folderId = $folder->id; + $folderHasAcl = $folder->acl; + $mountPoint = $folder->mountPoint; // ensure the trash folder exists - $this->setupTrashFolder($folderId, $user); + $this->setupTrashFolder($folder, $user); $trashFolder = $this->rootFolder->get('/' . $user->getUID() . '/files_trashbin/groupfolders/' . $folderId); $content = $trashFolder->getDirectoryListing(); $userCanManageAcl = $this->folderManager->canManageACL($folderId, $user); - $this->aclManagerFactory->getACLManager($user)->preloadRulesForFolder($this->getUnJailedPath($trashFolder)); + $this->aclManagerFactory->getACLManager($user)->preloadRulesForFolder($folder->storageId, $this->getUnJailedPath($trashFolder)); foreach ($content as $item) { /** @var \OC\Files\Node\Node $item */ $pathParts = pathinfo($item->getName()); @@ -468,7 +475,7 @@ private function getTrashForFolders(IUser $user, array $folders): array { continue; } - if (!$this->userHasAccessToPath($user, $this->getUnJailedPath($item))) { + if (!$this->userHasAccessToPath($folder->storageId, $user, $this->getUnJailedPath($item))) { continue; } @@ -477,7 +484,7 @@ private function getTrashForFolders(IUser $user, array $folders): array { $parentTrashItem = $trashItemsByOriginalLocation[$parentOriginalPath]; $relativePath = substr($originalLocation, strlen($parentOriginalPath)); $parentTrashItemPath = "__groupfolders/trash/{$parentTrashItem['folder_id']}/{$parentTrashItem['name']}.d{$parentTrashItem['deleted_time']}"; - if (!$this->userHasAccessToPath($user, $parentTrashItemPath . $relativePath)) { + if (!$this->userHasAccessToPath($folder->storageId, $user, $parentTrashItemPath . $relativePath)) { continue 2; } } @@ -494,6 +501,7 @@ private function getTrashForFolders(IUser $user, array $folders): array { $user, $mountPoint, $this->userManager->get($deletedBy), + $folder, ); } } @@ -534,7 +542,7 @@ public function getTrashNodeById(IUser $user, int $fileId): ?Node { $relativePath = $trashFolder->getRelativePath($absolutePath); [, $folderId, $nameAndTime] = explode('/', $relativePath); - if ($this->userHasAccessToFolder($user, (int)$folderId) && $this->userHasAccessToPath($user, $absolutePath)) { + if ($this->userHasAccessToFolder($user, (int)$folderId) && $this->userHasAccessToPath($storage->getCache()->getNumericStorageId(), $user, $absolutePath)) { return $trashFolder->get($relativePath); } else { return null; @@ -544,27 +552,30 @@ public function getTrashNodeById(IUser $user, int $fileId): ?Node { } } - public function cleanTrashFolder(int $folderid): void { - $trashFolder = $this->setupTrashFolder($folderid); + public function cleanTrashFolder(FolderDefinitionWithPermissions $folder): void { + $trashFolder = $this->setupTrashFolder($folder); foreach ($trashFolder->getDirectoryListing() as $node) { $node->delete(); } - $this->trashManager->emptyTrashbin($folderid); + $this->trashManager->emptyTrashbin($folder->id); } public function expire(Expiration $expiration): array { $size = 0; $count = 0; - $folders = $this->folderManager->getAllFoldersWithSize($this->rootFolder->getMountPoint()->getNumericStorageId()); + $folders = $this->folderManager->getAllFoldersWithSize(); + $folders = array_map(function (FolderWithMappingsAndCache $folder): FolderDefinitionWithPermissions { + return FolderDefinitionWithPermissions::fromFolder($folder, $folder->rootCacheEntry, Constants::PERMISSION_ALL); + }, $folders); foreach ($folders as $folder) { - $folderId = $folder['id']; + $folderId = $folder->id; $trashItems = $this->trashManager->listTrashForFolders([$folderId]); // calculate size of trash items $sizeInTrash = 0; - $trashFolder = $this->setupTrashFolder($folderId); + $trashFolder = $this->setupTrashFolder($folder); $nodes = []; // cache foreach ($trashItems as $groupTrashItem) { $nodeName = $groupTrashItem['name'] . '.d' . $groupTrashItem['deleted_time']; @@ -578,6 +589,8 @@ public function expire(Expiration $expiration): array { $sizeInTrash += $node->getSize(); } + $size = $folder->rootCacheEntry->getSize(); + foreach ($trashItems as $groupTrashItem) { $nodeName = $groupTrashItem['name'] . '.d' . $groupTrashItem['deleted_time']; if (!isset($nodes[$nodeName])) { @@ -586,7 +599,7 @@ public function expire(Expiration $expiration): array { $node = $nodes[$nodeName]; - if ($expiration->isExpired($groupTrashItem['deleted_time'], $folder['quota'] > 0 && $folder['quota'] < ($folder['size'] + $sizeInTrash))) { + if ($expiration->isExpired($groupTrashItem['deleted_time'], $folder->quota > 0 && $folder->quota < ($size + $sizeInTrash))) { $this->logger->debug('expiring ' . $node->getPath()); if ($node->getStorage()->unlink($node->getInternalPath()) === false) { $this->logger->error('Failed to remove item from trashbin: ' . $node->getPath()); @@ -596,7 +609,7 @@ public function expire(Expiration $expiration): array { // only count up after checking if removal is possible $count += 1; $size += $node->getSize(); - $folder['size'] -= $node->getSize(); + $size -= $node->getSize(); $node->getStorage()->getCache()->remove($node->getInternalPath()); $this->trashManager->removeItem($folderId, $groupTrashItem['name'], $groupTrashItem['deleted_time']); if (!is_null($groupTrashItem['file_id']) && !is_null($this->versionsBackend)) { @@ -615,7 +628,8 @@ public function expire(Expiration $expiration): array { } /** - * Cleanup trashbin of of groupfolders that have been deleted + * @param array $existingFolders + * Cleanup trashbin of of group folders that have been deleted */ private function cleanupDeletedFoldersTrash(array $existingFolders): void { $trashRoot = $this->getTrashRoot(); @@ -624,8 +638,8 @@ private function cleanupDeletedFoldersTrash(array $existingFolders): void { if (is_numeric($folderId)) { $folderId = (int)$folderId; if (!isset($existingFolders[$folderId])) { - $this->cleanTrashFolder($folderId); - $this->setupTrashFolder($folderId)->delete(); + $this->cleanTrashFolder($existingFolders[$folderId]); + $this->setupTrashFolder($existingFolders[$folderId])->delete(); } } } diff --git a/lib/Versions/GroupVersionsExpireManager.php b/lib/Versions/GroupVersionsExpireManager.php index 8e9878eab..40ba92990 100644 --- a/lib/Versions/GroupVersionsExpireManager.php +++ b/lib/Versions/GroupVersionsExpireManager.php @@ -14,14 +14,12 @@ use OCA\GroupFolders\Event\GroupVersionsExpireDeleteVersionEvent; use OCA\GroupFolders\Event\GroupVersionsExpireEnterFolderEvent; use OCA\GroupFolders\Folder\FolderManager; +use OCA\GroupFolders\Folder\FolderWithMappingsAndCache; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\FileInfo; use OCP\IUser; -/** - * @psalm-import-type InternalFolderOut from FolderManager - */ class GroupVersionsExpireManager { public function __construct( private readonly FolderManager $folderManager, @@ -33,13 +31,13 @@ public function __construct( } public function expireAll(): void { - $folders = $this->folderManager->getAllFolders(); - foreach ($folders as $folder) { - $this->dispatcher->dispatchTyped(new GroupVersionsExpireEnterFolderEvent($folder)); - $this->expireFolder($folder); - } + $folders = $this->folderManager->getAllFoldersWithSize(); + $this->expireFolders($folders); } + /** + * @param FolderWithMappingsAndCache[] $folders + */ public function expireFolders(array $folders): void { foreach ($folders as $folder) { $this->dispatcher->dispatchTyped(new GroupVersionsExpireEnterFolderEvent($folder)); @@ -47,11 +45,8 @@ public function expireFolders(array $folders): void { } } - /** - * @param InternalFolderOut $folder - */ - public function expireFolder(array $folder): void { - $view = new View('/__groupfolders/versions/' . $folder['id']); + public function expireFolder(FolderWithMappingsAndCache $folder): void { + $view = new View('/__groupfolders/versions/' . $folder->id); $files = $this->versionsBackend->getAllVersionedFiles($folder); /** @var IUser */ $dummyUser = new User('', null, $this->dispatcher); @@ -75,7 +70,7 @@ public function expireFolder(array $folder): void { } else { // source file no longer exists $this->dispatcher->dispatchTyped(new GroupVersionsExpireDeleteFileEvent($fileId)); - $this->versionsBackend->deleteAllVersionsForFile($folder['id'], $fileId); + $this->versionsBackend->deleteAllVersionsForFile($folder->id, $fileId); } } } diff --git a/lib/Versions/VersionsBackend.php b/lib/Versions/VersionsBackend.php index d598095dc..0deb641f7 100644 --- a/lib/Versions/VersionsBackend.php +++ b/lib/Versions/VersionsBackend.php @@ -16,7 +16,9 @@ use OCA\Files_Versions\Versions\IVersion; use OCA\Files_Versions\Versions\IVersionBackend; use OCA\Files_Versions\Versions\IVersionsImporterBackend; -use OCA\GroupFolders\Folder\FolderManager; +use OCA\GroupFolders\Folder\FolderDefinitionWithMappings; +use OCA\GroupFolders\Folder\FolderDefinitionWithPermissions; +use OCA\GroupFolders\Folder\FolderWithMappingsAndCache; use OCA\GroupFolders\Mount\GroupFolderStorage; use OCA\GroupFolders\Mount\GroupMountPoint; use OCA\GroupFolders\Mount\MountProvider; @@ -34,9 +36,6 @@ use OCP\IUserSession; use Psr\Log\LoggerInterface; -/** - * @psalm-import-type InternalFolderOut from FolderManager - */ class VersionsBackend implements IVersionBackend, IMetadataVersionBackend, IDeletableVersionBackend, INeedSyncVersionBackend, IVersionsImporterBackend { public function __construct( private readonly IRootFolder $rootFolder, @@ -245,12 +244,13 @@ public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): Fi } /** - * @param InternalFolderOut $folder + * @param FolderWithMappingsAndCache $folder * @return array */ - public function getAllVersionedFiles(array $folder): array { - $versionsFolder = $this->getVersionsFolder($folder['id']); - $mount = $this->mountProvider->getMount($folder['id'], '/dummyuser/files/' . $folder['mount_point'], Constants::PERMISSION_ALL, $folder['quota']); + public function getAllVersionedFiles(FolderDefinitionWithMappings $folder): array { + $versionsFolder = $this->getVersionsFolder($folder->id); + $folderWithPermissions = FolderDefinitionWithPermissions::fromFolder($folder, $folder->rootCacheEntry, Constants::PERMISSION_ALL); + $mount = $this->mountProvider->getMount($folderWithPermissions, '/dummyuser/files/' . $folder->mountPoint); if ($mount === null) { $this->logger->error('Tried to get all the versioned files from a non existing mountpoint'); return []; diff --git a/tests/ACL/ACLCacheWrapperTest.php b/tests/ACL/ACLCacheWrapperTest.php index f1525af69..40d54d4cd 100644 --- a/tests/ACL/ACLCacheWrapperTest.php +++ b/tests/ACL/ACLCacheWrapperTest.php @@ -30,8 +30,10 @@ protected function setUp(): void { $this->aclManager = $this->createMock(ACLManager::class); $this->aclManager->method('getACLPermissionsForPath') - ->willReturnCallback(fn (string $path) => $this->aclPermissions[$path] ?? Constants::PERMISSION_ALL); + ->willReturnCallback(fn (int $_storageId, string $path) => $this->aclPermissions[$path] ?? Constants::PERMISSION_ALL); $this->source = $this->createMock(ICache::class); + $this->source->method('getNumericStorageId') + ->willReturn(1); $this->cache = new ACLCacheWrapper($this->source, $this->aclManager, false); } diff --git a/tests/ACL/ACLManagerTest.php b/tests/ACL/ACLManagerTest.php index 7c35e8d57..aee68e2f0 100644 --- a/tests/ACL/ACLManagerTest.php +++ b/tests/ACL/ACLManagerTest.php @@ -15,8 +15,6 @@ use OCA\GroupFolders\ACL\UserMapping\IUserMappingManager; use OCA\GroupFolders\Trash\TrashManager; use OCP\Constants; -use OCP\Files\IRootFolder; -use OCP\Files\Mount\IMountPoint; use OCP\IUser; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; @@ -71,19 +69,12 @@ private function createMapping(string $id): IUserMapping&MockObject { } private function getAclManager(bool $perUserMerge = false): ACLManager { - $rootMountPoint = $this->createMock(IMountPoint::class); - $rootMountPoint->method('getNumericStorageId') - ->willReturn(1); - $rootFolder = $this->createMock(IRootFolder::class); - $rootFolder->method('getMountPoint') - ->willReturn($rootMountPoint); - - return new ACLManager($this->ruleManager, $this->trashManager, $this->userMappingManager, $this->logger, $this->user, fn (): IRootFolder&MockObject => $rootFolder, null, $perUserMerge); + return new ACLManager($this->ruleManager, $this->trashManager, $this->userMappingManager, $this->logger, $this->user, $perUserMerge); } public function testGetACLPermissionsForPathNoRules(): void { $this->rules = []; - $this->assertEquals(Constants::PERMISSION_ALL, $this->aclManager->getACLPermissionsForPath('foo')); + $this->assertEquals(Constants::PERMISSION_ALL, $this->aclManager->getACLPermissionsForPath(0, 'foo')); } public function testGetACLPermissionsForPath(): void { @@ -105,11 +96,11 @@ public function testGetACLPermissionsForPath(): void { new Rule($this->createMapping('1'), 10, Constants::PERMISSION_READ, 0) // remove read ], ]; - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE - Constants::PERMISSION_UPDATE, $this->aclManager->getACLPermissionsForPath('foo')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, $this->aclManager->getACLPermissionsForPath('foo/bar')); - $this->assertEquals(Constants::PERMISSION_ALL, $this->aclManager->getACLPermissionsForPath('foo/bar/sub')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE - Constants::PERMISSION_UPDATE - Constants::PERMISSION_READ, $this->aclManager->getACLPermissionsForPath('foo/blocked')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE - Constants::PERMISSION_UPDATE - Constants::PERMISSION_READ, $this->aclManager->getACLPermissionsForPath('foo/blocked2')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE - Constants::PERMISSION_UPDATE, $this->aclManager->getACLPermissionsForPath(0, 'foo')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, $this->aclManager->getACLPermissionsForPath(0, 'foo/bar')); + $this->assertEquals(Constants::PERMISSION_ALL, $this->aclManager->getACLPermissionsForPath(0, 'foo/bar/sub')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE - Constants::PERMISSION_UPDATE - Constants::PERMISSION_READ, $this->aclManager->getACLPermissionsForPath(0, 'foo/blocked')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE - Constants::PERMISSION_UPDATE - Constants::PERMISSION_READ, $this->aclManager->getACLPermissionsForPath(0, 'foo/blocked2')); } public function testGetACLPermissionsForPathInTrashbin(): void { @@ -137,7 +128,7 @@ public function testGetACLPermissionsForPathInTrashbin(): void { 'original_location' => 'subfolder/subfolder2', 'folder_id' => '1', ]); - $this->assertEquals(Constants::PERMISSION_ALL, $this->aclManager->getACLPermissionsForPath('__groupfolders/trash/1/subfolder2.d1700752274/coucou.md')); + $this->assertEquals(Constants::PERMISSION_ALL, $this->aclManager->getACLPermissionsForPath(0, '__groupfolders/trash/1/subfolder2.d1700752274/coucou.md')); } @@ -162,11 +153,11 @@ public function testGetACLPermissionsForPathPerUserMerge(): void { new Rule($this->createMapping('1'), 10, Constants::PERMISSION_READ, 0) // remove read ], ]; - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE - Constants::PERMISSION_UPDATE, $aclManager->getACLPermissionsForPath('foo')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, $aclManager->getACLPermissionsForPath('foo/bar')); - $this->assertEquals(Constants::PERMISSION_ALL, $aclManager->getACLPermissionsForPath('foo/bar/sub')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE - Constants::PERMISSION_UPDATE, $aclManager->getACLPermissionsForPath('foo/blocked')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE - Constants::PERMISSION_UPDATE - Constants::PERMISSION_READ, $aclManager->getACLPermissionsForPath('foo/blocked2')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE - Constants::PERMISSION_UPDATE, $aclManager->getACLPermissionsForPath(0, 'foo')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, $aclManager->getACLPermissionsForPath(0, 'foo/bar')); + $this->assertEquals(Constants::PERMISSION_ALL, $aclManager->getACLPermissionsForPath(0, 'foo/bar/sub')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE - Constants::PERMISSION_UPDATE, $aclManager->getACLPermissionsForPath(0, 'foo/blocked')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE - Constants::PERMISSION_UPDATE - Constants::PERMISSION_READ, $aclManager->getACLPermissionsForPath(0, 'foo/blocked2')); } public function testGetPermissionsForTree(): void { @@ -183,15 +174,15 @@ public function testGetPermissionsForTree(): void { new Rule($this->createMapping('2'), 10, Constants::PERMISSION_DELETE, Constants::PERMISSION_DELETE) // re-add delete ], ]; - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $this->aclManager->getPermissionsForTree('foo')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $this->aclManager->getPermissionsForTree('foo/bar')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $this->aclManager->getPermissionsForTree(0, 'foo')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $this->aclManager->getPermissionsForTree(0, 'foo/bar')); - $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath('foo')); - $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath('foo/bar')); - $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath('foo/bar/asd')); + $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath(0, 'foo')); + $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath(0, 'foo/bar')); + $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath(0, 'foo/bar/asd')); - $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getPermissionsForTree('foo')); - $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getPermissionsForTree('foo/bar')); + $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getPermissionsForTree(0, 'foo')); + $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getPermissionsForTree(0, 'foo/bar')); $this->rules = [ 'foo2' => [ @@ -205,12 +196,12 @@ public function testGetPermissionsForTree(): void { ], ]; - $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath('foo2')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getACLPermissionsForPath('foo2/bar')); - $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath('foo2/bar/asd')); + $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath(0, 'foo2')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getACLPermissionsForPath(0, 'foo2/bar')); + $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath(0, 'foo2/bar/asd')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getPermissionsForTree('foo2')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getPermissionsForTree('foo2/bar')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getPermissionsForTree(0, 'foo2')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getPermissionsForTree(0, 'foo2/bar')); $this->rules = [ 'foo3' => [ @@ -224,11 +215,11 @@ public function testGetPermissionsForTree(): void { ], ]; - $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath('foo3')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getACLPermissionsForPath('foo3/bar')); - $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath('foo3/bar/asd')); + $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath(0, 'foo3')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getACLPermissionsForPath(0, 'foo3/bar')); + $this->assertEquals(Constants::PERMISSION_ALL, $perUserAclManager->getACLPermissionsForPath(0, 'foo3/bar/asd')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getPermissionsForTree('foo3')); - $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getPermissionsForTree('foo3/bar')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getPermissionsForTree(0, 'foo3')); + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getPermissionsForTree(0, 'foo3/bar')); } } diff --git a/tests/ACL/ACLScannerTest.php b/tests/ACL/ACLScannerTest.php index 48b5c79d9..73ef65822 100644 --- a/tests/ACL/ACLScannerTest.php +++ b/tests/ACL/ACLScannerTest.php @@ -25,7 +25,7 @@ private function getAclManager(array $rules): ACLManager&MockObject { ->disableOriginalConstructor() ->getMock(); $manager->method('getACLPermissionsForPath') - ->willReturnCallback(fn (string $path) => $rules[$path] ?? Constants::PERMISSION_ALL); + ->willReturnCallback(fn (int $_storageId, string $path) => $rules[$path] ?? Constants::PERMISSION_ALL); return $manager; } @@ -55,6 +55,7 @@ public function testScanAclStorage(): void { 'storage' => $baseStorage, 'acl_manager' => $acls, 'in_share' => false, + 'storage_id' => $cache->getNumericStorageId(), ]); $scanner = $aclStorage->getScanner(); diff --git a/tests/ACL/ACLStorageWrapperTest.php b/tests/ACL/ACLStorageWrapperTest.php index e448d7b17..fd816ed7f 100644 --- a/tests/ACL/ACLStorageWrapperTest.php +++ b/tests/ACL/ACLStorageWrapperTest.php @@ -27,12 +27,13 @@ protected function setUp(): void { $this->aclManager = $this->createMock(ACLManager::class); $this->aclManager->method('getACLPermissionsForPath') - ->willReturnCallback(fn (string $path) => $this->aclPermissions[$path] ?? Constants::PERMISSION_ALL); + ->willReturnCallback(fn (int $_storageId, string $path) => $this->aclPermissions[$path] ?? Constants::PERMISSION_ALL); $this->source = new Temporary([]); $this->storage = new ACLStorageWrapper([ 'storage' => $this->source, 'acl_manager' => $this->aclManager, - 'in_share' => false + 'in_share' => false, + 'storage_id' => $this->source->getCache()->getNumericStorageId(), ]); } diff --git a/tests/ACL/RuleTest.php b/tests/ACL/RuleTest.php index d932def00..10717959a 100644 --- a/tests/ACL/RuleTest.php +++ b/tests/ACL/RuleTest.php @@ -13,7 +13,7 @@ use Test\TestCase; class RuleTest extends TestCase { - public function permissionsProvider(): array { + public static function permissionsProvider(): array { return [ [0b00000000, 0b00000000, 0b00000000, 0b00000000], [0b10101010, 0b00000000, 0b11110000, 0b10101010], //empty mask should have no effect @@ -31,7 +31,7 @@ public function testApplyPermissions(int $input, int $mask, int $permissions, in $this->assertEquals($expected, $rule->applyPermissions($input)); } - public function mergeRulesProvider(): array { + public static function mergeRulesProvider(): array { return [ [[ [0b00001111, 0b00000011], diff --git a/tests/Event/GroupVersionsExpireDeleteFileEventTest.php b/tests/Event/GroupVersionsExpireDeleteFileEventTest.php deleted file mode 100644 index 9d58a8864..000000000 --- a/tests/Event/GroupVersionsExpireDeleteFileEventTest.php +++ /dev/null @@ -1,20 +0,0 @@ -assertEquals(123, $event->fileId); - } -} diff --git a/tests/Event/GroupVersionsExpireDeleteVersionEventTest.php b/tests/Event/GroupVersionsExpireDeleteVersionEventTest.php deleted file mode 100644 index aa00bf47e..000000000 --- a/tests/Event/GroupVersionsExpireDeleteVersionEventTest.php +++ /dev/null @@ -1,27 +0,0 @@ -createMock(GroupVersion::class); - $version - ->expects($this->once()) - ->method('getFolderId') - ->willReturn(123); - - $event = new GroupVersionsExpireDeleteVersionEvent($version); - $this->assertEquals(123, $event->version->getFolderId()); - } -} diff --git a/tests/Event/GroupVersionsExpireEnterFolderEventTest.php b/tests/Event/GroupVersionsExpireEnterFolderEventTest.php deleted file mode 100644 index 65910a8ba..000000000 --- a/tests/Event/GroupVersionsExpireEnterFolderEventTest.php +++ /dev/null @@ -1,20 +0,0 @@ - 'value']); - $this->assertEquals(['key' => 'value'], $event->folder); - } -} diff --git a/tests/Folder/FolderManagerTest.php b/tests/Folder/FolderManagerTest.php index ec6554d90..864a406f2 100644 --- a/tests/Folder/FolderManagerTest.php +++ b/tests/Folder/FolderManagerTest.php @@ -8,8 +8,12 @@ namespace OCA\GroupFolders\Tests\Folder; +use OC\Files\Cache\CacheEntry; use OCA\GroupFolders\ACL\UserMapping\IUserMappingManager; +use OCA\GroupFolders\Folder\FolderDefinition; +use OCA\GroupFolders\Folder\FolderDefinitionWithPermissions; use OCA\GroupFolders\Folder\FolderManager; +use OCA\GroupFolders\Mount\FolderStorageManager; use OCA\GroupFolders\ResponseDefinitions; use OCP\Constants; use OCP\EventDispatcher\IEventDispatcher; @@ -37,6 +41,7 @@ class FolderManagerTest extends TestCase { private IEventDispatcher&MockObject $eventDispatcher; private IConfig&MockObject $config; private IUserMappingManager&MockObject $userMappingManager; + private FolderStorageManager $folderStorageManager; protected function setUp(): void { parent::setUp(); @@ -47,6 +52,8 @@ protected function setUp(): void { $this->eventDispatcher = $this->createMock(IEventDispatcher::class); $this->config = $this->createMock(IConfig::class); $this->userMappingManager = $this->createMock(IUserMappingManager::class); + $this->folderStorageManager = Server::get(FolderStorageManager::class); + $this->manager = new FolderManager( Server::get(IDBConnection::class), $this->groupManager, @@ -55,6 +62,7 @@ protected function setUp(): void { $this->eventDispatcher, $this->config, $this->userMappingManager, + $this->folderStorageManager, ); $this->clean(); } @@ -68,18 +76,14 @@ private function clean(): void { } /** - * @param list, acl?: bool, quota?: int, size?: int}> $folders + * @param list, acl?: bool, quota?: int, size?: int, root_id?: int, storage_id?: int}> $folders */ private function assertHasFolders(array $folders): void { $existingFolders = array_values($this->manager->getAllFolders()); - usort($existingFolders, fn (array $a, array $b): int => strcmp($a['mount_point'], $b['mount_point'])); + usort($existingFolders, fn (FolderDefinition $a, FolderDefinition $b): int => strcmp($a->mountPoint, $b->mountPoint)); usort($folders, fn (array $a, array $b): int => strcmp($a['mount_point'], $b['mount_point'])); foreach ($folders as &$folder) { - if (!isset($folder['size'])) { - $folder['size'] = 0; - } - if (!isset($folder['quota'])) { $folder['quota'] = FileInfo::SPACE_UNLIMITED; } @@ -89,9 +93,12 @@ private function assertHasFolders(array $folders): void { } } - foreach ($existingFolders as &$existingFolder) { - unset($existingFolder['id']); - } + $existingFolders = array_map(fn (FolderDefinition $existingFolder): array => [ + 'mount_point' => $existingFolder->mountPoint, + 'quota' => $existingFolder->quota, + 'acl' => $existingFolder->acl, + 'groups' => $existingFolder->groups, + ], $existingFolders); $this->assertEquals($folders, $existingFolders); } @@ -328,8 +335,8 @@ public function testGetFoldersForGroup(): void { $folders = $this->manager->getFoldersForGroup('g1'); $this->assertCount(1, $folders); $folder = $folders[0]; - $this->assertEquals('foo', $folder['mount_point']); - $this->assertEquals(2, $folder['permissions']); + $this->assertEquals('foo', $folder->mountPoint); + $this->assertEquals(2, $folder->permissions); } public function testGetFoldersForGroups(): void { @@ -346,14 +353,14 @@ public function testGetFoldersForGroups(): void { $folders = $this->manager->getFoldersForGroups(['g1']); $this->assertCount(1, $folders); $folder = $folders[0]; - $this->assertEquals('foo', $folder['mount_point']); - $this->assertEquals(2, $folder['permissions']); + $this->assertEquals('foo', $folder->mountPoint); + $this->assertEquals(2, $folder->permissions); } /** * @param string[] $groups */ - protected function getUser($groups = []): IUser&MockObject { + protected function getUser(array $groups = []): IUser&MockObject { $id = uniqid(); $user = $this->createMock(IUser::class); $this->groupManager->expects($this->any()) @@ -374,16 +381,22 @@ public function testGetFoldersForUserEmpty(): void { public function testGetFoldersForUserSimple(): void { $db = $this->createMock(IDBConnection::class); $manager = $this->getMockBuilder(FolderManager::class) - ->setConstructorArgs([$db, $this->groupManager, $this->mimeLoader, $this->logger, $this->eventDispatcher, $this->config, $this->userMappingManager]) + ->setConstructorArgs([$db, $this->groupManager, $this->mimeLoader, $this->logger, $this->eventDispatcher, $this->config, $this->userMappingManager, $this->folderStorageManager]) ->onlyMethods(['getFoldersForGroups']) ->getMock(); - $folder = [ - 'folder_id' => 1, - 'mount_point' => 'foo', - 'permissions' => 31, - 'quota' => FileInfo::SPACE_UNLIMITED, - ]; + $cacheEntry = $this->createMock(CacheEntry::class); + + $folder = new FolderDefinitionWithPermissions( + 1, + 'foo', + 1000, + false, + 1, + 2, + $cacheEntry, + 31, + ); $manager->expects($this->once()) ->method('getFoldersForGroups') @@ -396,57 +409,80 @@ public function testGetFoldersForUserSimple(): void { public function testGetFoldersForUserMerge(): void { $db = $this->createMock(IDBConnection::class); $manager = $this->getMockBuilder(FolderManager::class) - ->setConstructorArgs([$db, $this->groupManager, $this->mimeLoader, $this->logger, $this->eventDispatcher, $this->config, $this->userMappingManager]) + ->setConstructorArgs([$db, $this->groupManager, $this->mimeLoader, $this->logger, $this->eventDispatcher, $this->config, $this->userMappingManager, $this->folderStorageManager]) ->onlyMethods(['getFoldersForGroups']) ->getMock(); - $folder1 = [ - 'folder_id' => 1, - 'mount_point' => 'foo', - 'permissions' => 3, - 'quota' => 1000 - ]; - $folder2 = [ - 'folder_id' => 1, - 'mount_point' => 'foo', - 'permissions' => 8, - 'quota' => 1000 - ]; + $cacheEntry = $this->createMock(CacheEntry::class); + + $folder1 = new FolderDefinitionWithPermissions( + 1, + 'foo', + 1000, + false, + 1, + 2, + $cacheEntry, + 3, + ); + $folder2 = new FolderDefinitionWithPermissions( + 1, + 'foo', + 1000, + false, + 1, + 2, + $cacheEntry, + 8, + ); + $merged = new FolderDefinitionWithPermissions( + 1, + 'foo', + 1000, + false, + 1, + 2, + $cacheEntry, + 8 + 3, + ); $manager->expects($this->any()) ->method('getFoldersForGroups') ->willReturn([$folder1, $folder2]); $folders = $manager->getFoldersForUser($this->getUser(['g1', 'g2', 'g3'])); - $this->assertEquals([ - [ - 'folder_id' => 1, - 'mount_point' => 'foo', - 'permissions' => 11, - 'quota' => 1000 - ] - ], $folders); + $this->assertEquals([$merged], $folders); } public function testGetFolderPermissionsForUserMerge(): void { $db = $this->createMock(IDBConnection::class); $manager = $this->getMockBuilder(FolderManager::class) - ->setConstructorArgs([$db, $this->groupManager, $this->mimeLoader, $this->logger, $this->eventDispatcher, $this->config, $this->userMappingManager]) + ->setConstructorArgs([$db, $this->groupManager, $this->mimeLoader, $this->logger, $this->eventDispatcher, $this->config, $this->userMappingManager, $this->folderStorageManager]) ->onlyMethods(['getFoldersForGroups']) ->getMock(); - $folder1 = [ - 'folder_id' => 1, - 'mount_point' => 'foo', - 'permissions' => 3, - 'quota' => 1000 - ]; - $folder2 = [ - 'folder_id' => 1, - 'mount_point' => 'foo', - 'permissions' => 8, - 'quota' => 1000 - ]; + $cacheEntry = $this->createMock(CacheEntry::class); + + $folder1 = new FolderDefinitionWithPermissions( + 1, + 'foo', + 1000, + false, + 1, + 2, + $cacheEntry, + 3, + ); + $folder2 = new FolderDefinitionWithPermissions( + 1, + 'foo', + 1000, + false, + 1, + 2, + $cacheEntry, + 8, + ); $manager->expects($this->any()) ->method('getFoldersForGroups') @@ -470,12 +506,16 @@ public function testQuotaDefaultValue(): void { return 1024 ** ($exponent++); }); - /** @var array $folder */ $folder = $this->manager->getFolder($folderId1); - $this->assertEquals(1024 ** 3, $folder['quota']); + if (!$folder) { + throw new \Exception('Folder not found'); + } + $this->assertEquals(1024 ** 3, $folder->quota); - /** @var array $folder */ $folder = $this->manager->getFolder($folderId1); - $this->assertEquals(1024 ** 4, $folder['quota']); + if (!$folder) { + throw new \Exception('Folder not found'); + } + $this->assertEquals(1024 ** 4, $folder->quota); } } diff --git a/tests/Trash/TrashBackendTest.php b/tests/Trash/TrashBackendTest.php index 8dec722e7..742a71aca 100644 --- a/tests/Trash/TrashBackendTest.php +++ b/tests/Trash/TrashBackendTest.php @@ -16,6 +16,7 @@ use OCA\GroupFolders\ACL\Rule; use OCA\GroupFolders\ACL\RuleManager; use OCA\GroupFolders\ACL\UserMapping\UserMapping; +use OCA\GroupFolders\Folder\FolderDefinitionWithPermissions; use OCA\GroupFolders\Folder\FolderManager; use OCA\GroupFolders\Mount\GroupFolderStorage; use OCA\GroupFolders\Trash\TrashBackend; @@ -89,8 +90,12 @@ public function setUp(): void { } protected function tearDown(): void { - $this->trashBackend->cleanTrashFolder($this->folderId); - $this->folderManager->removeFolder($this->folderId); + $folder = $this->folderManager->getFolder($this->folderId); + if ($folder) { + $folderWithPermissions = FolderDefinitionWithPermissions::fromFolder($folder, $folder->rootCacheEntry, Constants::PERMISSION_ALL); + $this->trashBackend->cleanTrashFolder($folderWithPermissions); + $this->folderManager->removeFolder($this->folderId); + } /** @var SetupManager $setupManager */ $setupManager = \OCP\Server::get(SetupManager::class);