From 2a40298a302166865e8f4df766be8bf85ddfb46c Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Tue, 17 Jun 2025 01:47:46 +0200 Subject: [PATCH 1/3] feat: global internal link Signed-off-by: Maxence Lange --- appinfo/info.xml | 1 + appinfo/routes.php | 8 + lib/BackgroundJobs/UpdateLookupServer.php | 10 +- lib/Command/GlobalScaleDiscovery.php | 55 +++ lib/ConfigLexicon.php | 41 ++ lib/Controller/SlaveController.php | 84 +++- lib/Db/FileRequest.php | 187 ++++++++ lib/Db/ShareRequest.php | 129 ++++++ .../LocalFederatedShareException.php | 28 ++ lib/Exceptions/SharedFileException.php | 16 + lib/Lookup.php | 17 + lib/Master.php | 4 +- lib/Model/FederatedShare.php | 158 +++++++ lib/Model/LocalFile.php | 102 ++++ lib/Model/LocalMount.php | 58 +++ lib/PublicCapabilities.php | 7 + lib/Service/GlobalScaleService.php | 169 +++++++ lib/Service/GlobalShareService.php | 436 ++++++++++++++++++ 18 files changed, 1490 insertions(+), 20 deletions(-) create mode 100644 lib/Command/GlobalScaleDiscovery.php create mode 100644 lib/ConfigLexicon.php create mode 100644 lib/Db/FileRequest.php create mode 100644 lib/Db/ShareRequest.php create mode 100644 lib/Exceptions/LocalFederatedShareException.php create mode 100644 lib/Exceptions/SharedFileException.php create mode 100644 lib/Model/FederatedShare.php create mode 100644 lib/Model/LocalFile.php create mode 100644 lib/Model/LocalMount.php create mode 100644 lib/Service/GlobalScaleService.php create mode 100644 lib/Service/GlobalShareService.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 37522262..7ae59943 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -28,6 +28,7 @@ + OCA\GlobalSiteSelector\Command\GlobalScaleDiscovery OCA\GlobalSiteSelector\Command\UsersUpdate diff --git a/appinfo/routes.php b/appinfo/routes.php index e67133f5..6d1f187d 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -9,6 +9,8 @@ return [ 'ocs' => [ ['name' => 'Slave#createAppToken', 'url' => '/v1/createapptoken', 'verb' => 'GET'], + ['name' => 'Slave#discovery', 'url' => '/discovery', 'verb' => 'GET'], + ['name' => 'Slave#sharedFile', 'url' => '/sharedfile', 'verb' => 'GET'], ], 'routes' => [ [ @@ -21,5 +23,11 @@ 'url' => '/autologout', 'verb' => 'GET' ], + [ + 'name' => 'Slave#findFile', + 'url' => '/gf/{token}/{fileId}', + 'verb' => 'GET', + 'root' => '', + ], ], ]; diff --git a/lib/BackgroundJobs/UpdateLookupServer.php b/lib/BackgroundJobs/UpdateLookupServer.php index e3071ffc..4d92de88 100644 --- a/lib/BackgroundJobs/UpdateLookupServer.php +++ b/lib/BackgroundJobs/UpdateLookupServer.php @@ -6,10 +6,10 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCA\GlobalSiteSelector\BackgroundJobs; use OCA\GlobalSiteSelector\GlobalSiteSelector; +use OCA\GlobalSiteSelector\Service\GlobalScaleService; use OCA\GlobalSiteSelector\Slave; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJob; @@ -17,13 +17,12 @@ use OCP\IConfig; class UpdateLookupServer extends TimedJob { - - public function __construct( ITimeFactory $time, IConfig $config, - private GlobalSiteSelector $globalSiteSelector, - private Slave $slave, + private readonly GlobalScaleService $globalScaleService, + private readonly GlobalSiteSelector $globalSiteSelector, + private readonly Slave $slave, ) { parent::__construct($time); @@ -36,6 +35,7 @@ protected function run($argument) { return; } + $this->globalScaleService->refreshTokenFromGlobalScale(); $this->slave->batchUpdate(); } } diff --git a/lib/Command/GlobalScaleDiscovery.php b/lib/Command/GlobalScaleDiscovery.php new file mode 100644 index 00000000..07754db0 --- /dev/null +++ b/lib/Command/GlobalScaleDiscovery.php @@ -0,0 +1,55 @@ +setName('globalsiteselector:discovery') + ->addOption('current', '', InputOption::VALUE_NONE, 'display current data') + ->setDescription('run a discovery request over Global Scale to get details about each instances'); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + if ($input->getOption('current')) { + $output->writeln(json_encode($this->appConfig->getValueArray(Application::APP_ID, ConfigLexicon::GS_TOKENS), JSON_PRETTY_PRINT)); + return 0; + } + + // currently, the only available data is a unique token that helps identify each instance + $this->globalScaleService->refreshTokenFromGlobalScale(); + return 0; + } +} diff --git a/lib/ConfigLexicon.php b/lib/ConfigLexicon.php new file mode 100644 index 00000000..60ce602f --- /dev/null +++ b/lib/ConfigLexicon.php @@ -0,0 +1,41 @@ + $this->globalScaleService->getLocalToken()]); + } + + /** + * return sharing details about a file. + * request must contain encoded jwt. + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function findFile(string $token, int $fileId): RedirectResponse { + return new RedirectResponse($this->urlGenerator->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $this->globalShareService->getNewFileId($token, $fileId) ?? 1])); + } + + /** + * return sharing details about a file. + * request must contain encoded jwt. + */ + #[PublicPage] + #[NoCSRFRequired] + public function sharedFile(string $jwt): DataResponse { + $key = $this->gss->getJwtKey(); + $decoded = (array)JWT::decode($jwt, new Key($key, Application::JWT_ALGORITHM)); + // JWT store data as stdClass, not array + $decoded = json_decode(json_encode($decoded), true); + + $this->logger->debug('decoded request', ['data' => $decoded]); + + $fileId = (int)($decoded['fileId'] ?? 0); + $shareId = (int)($decoded['shareId'] ?? 0); + $instance = $decoded['instance'] ?? ''; + + $target = new LocalFile(); + $target->import($decoded['target'] ?? []); + + try { + // the file is local and returns shares related to it + return new DataResponse($this->globalShareService->getSharedFiles($fileId, $shareId, $instance, $target)); + } catch (SharedFileException $e) { + // file not found + return new DataResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } catch (LocalFederatedShareException $e) { + // the file is not local and returns the shared folder and the path to the file + return new DataResponse($e->getFederatedShare(), Http::STATUS_MOVED_PERMANENTLY); + } + } + + /** * @PublicPage * @NoCSRFRequired diff --git a/lib/Db/FileRequest.php b/lib/Db/FileRequest.php new file mode 100644 index 00000000..eccb311f --- /dev/null +++ b/lib/Db/FileRequest.php @@ -0,0 +1,187 @@ +connection->getQueryBuilder(); + $qb->select('parent', 'name', 'storage') + ->from('filecache') + ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + + $result = $qb->executeQuery(); + $row = $result->fetch(); + if ($row === false) { + return null; + } + $details = new LocalFile(); + $details->setId($fileId) + ->setName($row['name'] ?? '') + ->setStorageId($row['storage'] ?? -1) + ->setParent($row['parent'] ?? -1); + $result->closeCursor(); + + return $details; + } + + /** + * return details about the mount point from a LocalFile + */ + public function getMountFromTarget(LocalFile $target): ?LocalMount { + $qb = $this->connection->getQueryBuilder(); + $qb->select('mount_provider_class', 'mount_point', 'user_id') + ->from('mounts') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('storage_id', $qb->createNamedParameter($target->getStorageId(), IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('root_id', $qb->createNamedParameter($target->getId(), IQueryBuilder::PARAM_INT)), + ) + ); + + $result = $qb->executeQuery(); + $row = $result->fetch(); + if ($row === false) { + return null; + } + + $mount = new LocalMount(); + $mount->setProviderClass($row['mount_provider_class']) + ->setMountPoint(rtrim(explode('/files', $row['mount_point'], 2)[1] ?? '', '/')) + ->setUserId($row['user_id']); + + $result->closeCursor(); + + return $mount; + } + + /** + * returns remote details about a team share mount point + */ + public function getFederatedTeamMount(LocalMount $mount, array $teamIds): ?FederatedShare { + $qb = $this->connection->getQueryBuilder(); + $qb->select('remote', 'remote_id') + ->from('circles_mount') + ->where( + $qb->expr()->eq('mountpoint_hash', $qb->createNamedParameter(md5($mount->getMountPoint()))), + $qb->expr()->in('circle_id', $qb->createNamedParameter($teamIds, IQueryBuilder::PARAM_STR_ARRAY)), + ); + + $result = $qb->executeQuery(); + $row = $result->fetch(); + if ($row === false || ($row['remote'] ?? '') === '') { + return null; + } + + $federatedShare = new FederatedShare(); + $federatedShare->setRemote($row['remote']) + ->setRemoteId($row['remote_id']) + ->setBounce(true); + + $result->closeCursor(); + + return $federatedShare; + } + + /** + * returns id from a storage mount point + */ + public function getFilesFromExternalShareStorage(string $storageKey): int { + $qb = $this->connection->getQueryBuilder(); + $qb->select('c.fileid') + ->from('filecache', 'c') + ->from('storages', 's') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('s.numeric_id', 'c.storage'), + $qb->expr()->eq('s.id', $qb->createNamedParameter($storageKey)), + $qb->expr()->eq('c.parent', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT)), + ) + ); + + $result = $qb->executeQuery(); + $row = $result->fetch(); + if ($row !== false) { + $fileId = (int)$row['fileid']; + } + $result->closeCursor(); + + return $fileId ?? 0; + } + + /** + * returns the storage key related to federated share from share_external + */ + public function getFederatedShareStorageKey(FederatedShare $federatedShare, string $instance): ?string { + $qb = $this->connection->getQueryBuilder(); + $qb->select('share_token', 'owner', 'remote') + ->from('share_external') + ->where( + $qb->expr()->andX( + $qb->expr()->like('remote', $qb->createNamedParameter('%://' . $instance . '/')), + $qb->expr()->eq('remote_id', $qb->createNamedParameter($federatedShare->getId(), IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('user', $qb->createNamedParameter($federatedShare->getShareWith())) + ) + ); + + $result = $qb->executeQuery(); + $row = $result->fetch(); + if ($row === false) { + return null; + } + $cloudId = $this->cloudIdManager->getCloudId($row['owner'], $row['remote']); + $storage = 'shared::' . md5($row['share_token'] . '@' . $cloudId->getRemote()); + $result->closeCursor(); + + return $storage; + } + + /** + * returns the storage key related to a federated share from circles_mount + */ + public function getTeamStorages(FederatedShare $federatedShare, string $instance): ?string { + $qb = $this->connection->getQueryBuilder(); + $qb->select('token', 'remote') + ->from('circles_mount') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('remote', $qb->createNamedParameter($instance)), + $qb->expr()->eq('remote_id', $qb->createNamedParameter($federatedShare->getId(), IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('circle_id', $qb->createNamedParameter($federatedShare->getShareWith())) + ) + ); + + $result = $qb->executeQuery(); + $row = $result->fetch(); + if ($row === false) { + return null; + } + // why not storing md5 into circles_mount ? + $storage = 'shared::' . md5($row['token'] . '@https://' . $row['remote']); + $result->closeCursor(); + + return $storage; + } +} diff --git a/lib/Db/ShareRequest.php b/lib/Db/ShareRequest.php new file mode 100644 index 00000000..6d9bcc6d --- /dev/null +++ b/lib/Db/ShareRequest.php @@ -0,0 +1,129 @@ +getId()] = $entry; + $ids[] = $entry->getId(); + } + + $qb = $this->connection->getQueryBuilder(); + $qb->select('s.id', 's.file_source', 's.share_type', 's.share_with', 's.permissions') + ->from('share', 's') + ->where( + $qb->expr()->andX( + $qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)), + $qb->expr()->orX( + $qb->expr()->andX( + $qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_REMOTE, IShare::TYPE_REMOTE_GROUP], IQueryBuilder::PARAM_INT_ARRAY)), + $qb->expr()->like('share_with', $qb->createNamedParameter('%@' . $instance)), + ), + $qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_CIRCLE], IQueryBuilder::PARAM_INT_ARRAY)), + ) + ) + ); + + $result = $qb->executeQuery(); + $shares = []; + while ($row = $result->fetch()) { + $shareWith = $row['share_with']; + if (str_ends_with(strtolower($shareWith), '@' . strtolower($instance))) { + $shareWith = substr($shareWith, 0, -strlen('@' . $instance)); + } + + $federatedShare = new FederatedShare(); + $federatedShare->setId($row['id']) + ->setFileId($row['file_source']) + ->setShareType($row['share_type']) + ->setShareWith($shareWith) + ->setPermissions($row['permissions']) + ->setTarget($indexedFiles[$row['file_source']]); + $shares[] = $federatedShare; + } + $result->closeCursor(); + + return $shares; + } + + /** + * return id and owner about a file. + * + * @return array{int, string} [fileId, fileOwner] + */ + public function getFileOwnerFromShareId(int $shareId): array { + $qb = $this->connection->getQueryBuilder(); + $qb->select('uid_owner', 'file_source') + ->from('share', 's') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId, IQueryBuilder::PARAM_INT))); + + $result = $qb->executeQuery(); + $row = $result->fetch(); + if ($row === false) { + return []; + } + $fileId = (int)$row['file_source']; + $owner = $row['uid_owner']; + $result->closeCursor(); + + return [$fileId, $owner]; + } + + /** + * returns details about the remote share linked to a local mount and + * how it is identified by the remote instance + */ + public function getBouncedShareFromLocalMount(LocalMount $mount): ?FederatedShare { + $qb = $this->connection->getQueryBuilder(); + $qb->select('remote', 'remote_id') + ->from('share_external') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('user', $qb->createNamedParameter($mount->getUserId())), + $qb->expr()->eq('mountpoint_hash', $qb->createNamedParameter(md5($mount->getMountPoint()))), + ) + ); + + $result = $qb->executeQuery(); + $row = $result->fetch(); + if ($row === false) { + return null; + } + $bouncedShare = new FederatedShare(); + $bouncedShare->setBounce(true) + ->setRemote($row['remote']) + ->setRemoteId((int)$row['remote_id']); + $result->closeCursor(); + + return $bouncedShare; + } +} diff --git a/lib/Exceptions/LocalFederatedShareException.php b/lib/Exceptions/LocalFederatedShareException.php new file mode 100644 index 00000000..174100d7 --- /dev/null +++ b/lib/Exceptions/LocalFederatedShareException.php @@ -0,0 +1,28 @@ +federatedShare; + } +} diff --git a/lib/Exceptions/SharedFileException.php b/lib/Exceptions/SharedFileException.php new file mode 100644 index 00000000..eb94baac --- /dev/null +++ b/lib/Exceptions/SharedFileException.php @@ -0,0 +1,16 @@ +lookupServerUrl = $this->config->getSystemValueString('lookup_server', ''); @@ -148,6 +150,21 @@ private function getUserLocation_Sanitize(string $address, string &$uid): string return $address; } + /** + * get addresses of each instance of the global scale from lus + * + * @return string[] + */ + public function getInstances(): array { + $client = $this->clientService->newClient(); + $response = $client->get($this->lookupServerUrl . '/gs/instances', $this->configureClient(['body' => json_encode(['authKey' => $this->gss->getJwtKey()])])); + + try { + return json_decode($response->getBody(), true, flags: JSON_THROW_ON_ERROR); + } catch (JsonException) { + return []; + } + } public function sanitizeUid(string &$uid = ''): void { if ($this->config->getSystemValueString('gss.username_format', '') !== 'sanitize') { diff --git a/lib/Master.php b/lib/Master.php index ca3a0609..3975b804 100644 --- a/lib/Master.php +++ b/lib/Master.php @@ -328,9 +328,7 @@ protected function getAppToken($location, $uid, $password, $options) { $data = json_decode($body, true); $jsonErrorCode = json_last_error(); if ($jsonErrorCode !== JSON_ERROR_NONE) { - $info = 'getAppToken - Decoding the JSON failed ' - . $jsonErrorCode . ' ' - . json_last_error_msg(); + $info = 'getAppToken - Decoding the JSON failed ' . $jsonErrorCode . ' '. json_last_error_msg(); throw new Exception($info); } if (!isset($data['ocs']['data']['token'])) { diff --git a/lib/Model/FederatedShare.php b/lib/Model/FederatedShare.php new file mode 100644 index 00000000..66d017d8 --- /dev/null +++ b/lib/Model/FederatedShare.php @@ -0,0 +1,158 @@ +id = $id; + return $this; + } + + public function getId(): int { + return $this->id; + } + + public function setFileId(int $fileId): self { + $this->fileId = $fileId; + return $this; + } + + public function getFileId(): int { + return $this->fileId; + } + + public function setShareType(int $shareType): self { + $this->shareType = $shareType; + return $this; + } + + public function getShareType(): int { + return $this->shareType; + } + + public function setShareWith(string $shareWith): self { + $this->shareWith = $shareWith; + return $this; + } + + public function getShareWith(): string { + return $this->shareWith; + } + + public function setPermissions(int $permissions): self { + $this->permissions = $permissions; + return $this; + } + + public function getPermissions(): int { + return $this->permissions; + } + + public function setTarget(LocalFile $target): self { + $this->target = $target; + return $this; + } + + public function getTarget(): ?LocalFile { + return $this->target; + } + + public function setBounce(bool $bounce): self { + $this->bounce = $bounce; + return $this; + } + + public function isBounce(): bool { + return $this->bounce; + } + + public function setRemote(string $remote): self { + $this->remote = $remote; + return $this; + } + + public function getRemote(): string { + return $this->remote; + } + + public function setRemoteId(int $remoteId): self { + $this->remoteId = $remoteId; + return $this; + } + + public function getRemoteId(): int { + return $this->remoteId; + } + + /** + * deserialize model + */ + public function import(array $data): self { + $this->setBounce($data['bounce'] ?? false); + if ($this->isBounce()) { + $this->setRemoteId($data['remoteId'] ?? 0) + ->setRemote($data['remote'] ?? ''); + } else { + $this->setId($data['id'] ?? 0) + ->setFileId($data['fileId'] ?? 0) + ->setShareType($data['shareType'] ?? 0) + ->setShareWith($data['shareWith'] ?? '') + ->setPermissions($data['permissions'] ?? 0); + } + + if (array_key_exists('target', $data)) { + $target = new LocalFile(); + $target->import($data['target']); + $this->setTarget($target); + } + + return $this; + } + + /** + * @return array{id: int, fileId: int, shareType: int, shareWith: string, permissions: int, target: array, remote: string, remoteId: int} + */ + public function jsonSerialize(): array { + if ($this->isBounce()) { + return [ + 'remote' => $this->getRemote(), + 'remoteId' => $this->getRemoteId(), + 'target' => $this->getTarget(), + 'bounce' => $this->isBounce(), + ]; + } + + return [ + 'id' => $this->getId(), + 'fileId' => $this->getFileId(), + 'shareType' => $this->getShareType(), + 'shareWith' => $this->getShareWith(), + 'permissions' => $this->getPermissions(), + 'target' => $this->getTarget(), + ]; + + } +} diff --git a/lib/Model/LocalFile.php b/lib/Model/LocalFile.php new file mode 100644 index 00000000..f7e43941 --- /dev/null +++ b/lib/Model/LocalFile.php @@ -0,0 +1,102 @@ +id; + } + + public function setId(int $id): self { + $this->id = $id; + return $this; + } + + public function getName(): string { + return $this->name; + } + + public function setName(string $name): self { + $this->name = $name; + return $this; + } + + public function getStorageId(): int { + return $this->storageId; + } + + public function setStorageId(int $storageId): self { + $this->storageId = $storageId; + return $this; + } + + public function getParent(): int { + return $this->parent; + } + + public function setParent(int $parent): self { + $this->parent = $parent; + return $this; + } + + /** + * @return string[] + */ + public function getPath(): array { + return $this->path; + } + + /** + * @param string[] $path + * + * @return $this + */ + public function setPath(array $path): self { + $this->path = $path; + return $this; + } + + /** + * deserialize model + */ + public function import(array $data): self { + $this->setId($data['id'] ?? 0) + ->setName($data['name'] ?? '') + ->setStorageId($data['storageId'] ?? -1) + ->setParent($data['parent'] ?? -1) + ->setPath($data['path'] ?? []); + + return $this; + } + + /** + * @return array{id: int, name: string, storageId: int, parent: int, path: string[]} + */ + public function jsonSerialize(): array { + return [ + 'id' => $this->getId(), + 'name' => $this->getName(), + 'storageId' => $this->getStorageId(), + 'parent' => $this->getParent(), + 'path' => $this->getPath(), + ]; + } +} diff --git a/lib/Model/LocalMount.php b/lib/Model/LocalMount.php new file mode 100644 index 00000000..2bbeb879 --- /dev/null +++ b/lib/Model/LocalMount.php @@ -0,0 +1,58 @@ +providerClass = $providerClass; + return $this; + } + + public function getProviderClass(): string { + return $this->providerClass; + } + + public function setMountPoint(string $mountPoint): self { + $this->mountPoint = $mountPoint; + return $this; + } + + public function getMountPoint(): string { + return $this->mountPoint; + } + + public function setUserId(string $userId): self { + $this->userId = $userId; + return $this; + } + + public function getUserId(): string { + return $this->userId; + } + + /** + * @return array{provider: string, mountPoint: string, userId: string} + */ + public function jsonSerialize(): array { + return [ + 'provider' => $this->getProviderClass(), + 'mountPoint' => $this->getMountPoint(), + 'userId' => $this->getUserId(), + ]; + } +} diff --git a/lib/PublicCapabilities.php b/lib/PublicCapabilities.php index c7bfd041..4fc8c270 100644 --- a/lib/PublicCapabilities.php +++ b/lib/PublicCapabilities.php @@ -8,14 +8,21 @@ namespace OCA\GlobalSiteSelector; +use OCA\GlobalSiteSelector\Service\GlobalScaleService; use OCP\Capabilities\IPublicCapability; class PublicCapabilities implements IPublicCapability { + public function __construct( + private readonly GlobalScaleService $globalScaleService, + ) { + + } public function getCapabilities(): array { return [ 'globalscale' => [ 'enabled' => true, 'desktoplogin' => 1, + 'token' => $this->globalScaleService->getLocalToken(), ] ]; } diff --git a/lib/Service/GlobalScaleService.php b/lib/Service/GlobalScaleService.php new file mode 100644 index 00000000..5bb1aff7 --- /dev/null +++ b/lib/Service/GlobalScaleService.php @@ -0,0 +1,169 @@ +appConfig->hasKey(Application::APP_ID, ConfigLexicon::LOCAL_TOKEN)) { + $this->appConfig->setValueString(Application::APP_ID, ConfigLexicon::LOCAL_TOKEN, $this->secureRandom->generate(5, 'abcdefghijklmnopqrstuvwxyz0123456789')); + } + + return $this->appConfig->getValueString(Application::APP_ID, ConfigLexicon::LOCAL_TOKEN); + } + + /** + * return local address as known by lus + */ + public function getLocalAddress(): ?string { + return $this->getAddressFromToken($this->getLocalToken()); + } + + /** + * confirm a specific global scale token identify local instance + */ + public function isLocalToken(string $token): bool { + return ($this->appConfig->getValueString(Application::APP_ID, ConfigLexicon::LOCAL_TOKEN) === $token); + } + + /** + * confirm that a url (or a host) is related to local instance + */ + public function isLocalAddress(string $address): bool { + if (str_contains($address, '://')) { + $address = parse_url($address, PHP_URL_HOST); + } + return ($this->getLocalAddress() === $address); + } + + /** + * get global scale identity token from each instance of the global scale + */ + public function refreshTokenFromGlobalScale(): void { + if (!$this->gss->isSlave()) { + return; + } + + foreach ($this->lookup->getInstances() as $address) { + $this->refreshTokenFromAddress($address); + } + } + + /** + * request global scale token from a remote instance using public discovery and store it in local cache + */ + public function refreshTokenFromAddress(string $address): void { + if (!$this->gss->isSlave()) { + return; + } + + $token = $this->getRemotePublicDiscovery($address)['token'] ?? ''; + if ($token === '' || strlen($token) < 5) { + return; + } + + $tokens = $this->appConfig->getValueArray(Application::APP_ID, ConfigLexicon::GS_TOKENS); + if (($tokens[$address] ?? '') === $token) { + return; + } + + $tokens[$address] = $token; + $this->appConfig->setValueArray(Application::APP_ID, ConfigLexicon::GS_TOKENS, $tokens); + } + + /** + * get address from a global scale token + */ + public function getAddressFromToken(string $token): ?string { + $tokens = $this->appConfig->getValueArray(Application::APP_ID, ConfigLexicon::GS_TOKENS); + $address = array_search($token, $tokens, true); + if (!$address) { + return null; + } + return $address; + } + + /** + * returns global scale token from a specific address + */ + public function getTokenFromAddress(string $address): ?string { + $tokens = $this->appConfig->getValueArray(Application::APP_ID, ConfigLexicon::GS_TOKENS); + return $tokens[$address] ?? null; + } + + /** + * returns discovery data from a remote address + */ + public function getRemotePublicDiscovery(string $address): array { + return $this->requestGssOcs($address, 'Slave.discovery'); + } + + /** + * get data from a remote globalsiteselector ocs endpoint. + * + * @param string $address remote global scale instance + * @param string $route route name to the ocs endpoint + * @param array $data added to the request + * @param int $responseCode contains the response code from the request + * + * @return array decoded version of the json response + */ + public function requestGssOcs(string $address, string $route, array $data = [], int &$responseCode = 0): array { + $client = $this->clientService->newClient(); + try { + $response = $client->get( + 'https://' . $address . parse_url($this->urlGenerator->linkToOCSRouteAbsolute('globalsiteselector.' . $route), PHP_URL_PATH), + [ + 'headers' => ['OCS-APIRequest' => 'true'], + 'verify' => !$this->config->getSystemValueBool('gss.selfsigned.allow', false), + 'query' => array_merge($data, ['format' => 'json']) + ] + ); + } catch (Exception $e) { + $this->logger->warning('could not reach remote gss ocs', ['exception' => $e]); + return []; + } + + try { + $responseCode = $response->getStatusCode(); + return json_decode($response->getBody(), true, flags: JSON_THROW_ON_ERROR)['ocs']['data'] ?? []; + } catch (JsonException $e) { + $this->logger->warning('could not decode json', ['exception' => $e]); + return []; + } + } +} diff --git a/lib/Service/GlobalShareService.php b/lib/Service/GlobalShareService.php new file mode 100644 index 00000000..5f859e28 --- /dev/null +++ b/lib/Service/GlobalShareService.php @@ -0,0 +1,436 @@ +userSession->getUser()?->getUID(); + // There is no valid reason for getUser() to be null, + if ($currentUser === null) { + $this->logger->warning('internal link request', ['exception' => new \Exception('could not assign current user')]); + return null; + } + + // if token represents the local instance, fall back to normal behavior using file id + if ($this->globalScaleService->isLocalToken($token)) { + try { + $this->getSharedFiles($fileId); + return $fileId; + } catch (SharedFileException) { + return null; + } catch (LocalFederatedShareException $e) { + // file is not local + $federatedShare = $e->getFederatedShare(); + $remote = $federatedShare->getRemote(); + + // this should never be the case, but it confirms the file is not local + if (!$federatedShare->isBounce() || $this->globalScaleService->isLocalAddress($remote)) { + return null; + } + + // Get the list of federated shares between both instances that would provide access to the file. + // The file is identified by share mount point id and path to the final file. + $federatedShares = $this->requestRemoteFederatedShares($remote, ['shareId' => $federatedShare->getRemoteId(), 'target' => $federatedShare->getTarget()?->jsonSerialize() ?? []], true); + return $this->getLastFileIdFromShares($currentUser, $federatedShares, $remote); + } + } + + // extract instance linked to token + $instance = $this->globalScaleService->getAddressFromToken($token); + + // unknown instance, make it file not found + if ($instance === null) { + return null; + } + + // request the remote instance to get the list of existing federated shares between both instances and about the remote file id + return $this->getSharedFileRemoteDetails($instance, $fileId); + } + + + /** + * @param string|null $instance set to NULL when assuming local + * @return FederatedShare[] + * @throws SharedFileException + * @throws LocalFederatedShareException + */ + public function getSharedFiles(int $fileId, int $shareId = 0, ?string $instance = null, ?LocalFile $target = null): array { + // in case of redirection, we get the final file id from share mount id and path to the file + if ($shareId > 0 && $target !== null) { + $fileId = $this->getIdFromSharedTarget($shareId, $target); + } + + if ($fileId === 0 || $instance === '') { + throw new SharedFileException('missing argument'); + } + + // from a file id, get all parents until mount point + $files = $this->getRelatedFiles((int)$fileId); + if (empty($files)) { + throw new SharedFileException('file not found'); + } + + // based on the mount point (last element of the list, top parent folder) we know if the file is local or a federated share from another instance + $mountPoint = array_slice($files, -1)[0]; + + // In case the mount point is a remote share, we send the correct remote instance and the remote share id + $remoteShare = $this->getFederatedShareFromTargetLocalFile($mountPoint); + + if ($remoteShare?->isBounce() === true) { + // from the base mount point we add the target to reach the destination filew + $remoteShare->setTarget($mountPoint); + throw new LocalFederatedShareException($remoteShare); + } + + if ($instance === null) { + return []; + } + + // mount point is local, we return the list of shares between the remote instance and the related files + return $this->shareRequest->getFederatedSharesRelatedToRemoteInstance($files, $instance); + } + + + /** + * get details about a shared remote file based on the address of the remote + * instance and the id of the file as stored on that remote instance + * + * @param string $remote address of the remote instance + * @param int $remoteFileId id of the file as stored on the remote instance + * @return int local file id, 1 if not found + */ + private function getSharedFileRemoteDetails(string $remote, int $remoteFileId): int { + $currentUser = $this->userSession->getUser()?->getUID(); + if ($currentUser === null || $this->globalScaleService->getLocalAddress() === null) { + return 1; + } + + try { + $federatedShares = $this->requestRemoteFederatedShares($remote, ['fileId' => $remoteFileId]); + } catch (LocalFederatedShareException $e) { + // share is local, meaning we should be able to locally find the id of the file + $federatedShare = $e->getFederatedShare(); + [$fileId, $fileOwner] = $this->shareRequest->getFileOwnerFromShareId($federatedShare->getRemoteId()); + return $this->getFinalFileId($fileOwner, $fileId, $federatedShare->getTarget()); + } + + if (empty($federatedShares)) { + return 1; + } + + $this->logger->warning('federated shares', ['remote' => $remote, 'remoteFileId' => $remoteFileId, 'federatedShares' => json_decode(json_encode($federatedShares), true)]); + + return $this->getLastFileIdFromShares($currentUser, $federatedShares, $remote); + } + + /** + * returns details about a local file and (recursively) about all parent folders + * + * @return LocalFile[] + */ + private function getRelatedFiles(int $fileId): array { + if ($fileId === 0) { + return []; + } + + $files = $path = []; + for ($i = 0; $i < self::LIMIT_PARENTS; $i++) { + $fileDetails = $this->fileRequest->getFileDetails($fileId); + if ($fileDetails === null) { + break; + } + + $fileId = $fileDetails->getParent(); + $fileDetails->setPath($path); + $files[] = $fileDetails; + if ($fileId === -1) { + break; + } + $path[] = $fileDetails->getName(); + } + + return $files; + } + + /** + * returns details about the remote share linked to a local mount, how + * it is identified by the remote instance and the path to get back to + * the target file from the mount point. + * + * @return FederatedShare|null if no federated share were found + */ + private function getFederatedShareFromTargetLocalFile(LocalFile $target): ?FederatedShare { + $mount = $this->fileRequest->getMountFromTarget($target); + if ($mount === null) { + return null; + } + + if ($mount->getProviderClass() === '') { + // could be from a federated team + $teamMount = $this->fileRequest->getFederatedTeamMount($mount, $this->getCurrentTeams($mount->getUserId())); + if ($teamMount !== null) { + return $teamMount->setTarget($target); + } + } + + if ($mount->getProviderClass() !== MountProvider::class) { + return null; + } + + $federatedShare = $this->shareRequest->getBouncedShareFromLocalMount($mount); + $federatedShare?->setTarget($target); + + return $federatedShare; + } + + private function getIdFromSharedTarget(int $shareId, LocalFile $target): int { + [$fileId, $fileOwner] = $this->shareRequest->getFileOwnerFromShareId($shareId); + return $this->getFinalFileId($fileOwner, $fileId, $target); + } + + /** + * Return a file id based on a list of available shares. + * A preferred share is selected based on permissions. + * Path will be applied to share mount point. + * + * @param FederatedShare[] $federatedShares + */ + private function getLastFileIdFromShares(string $userId, array $federatedShares, string $instance): int { + if (str_contains($instance, '://')) { + $instance = parse_url($instance, PHP_URL_HOST); + } + + $permission = -1; + $higherPermissionShare = null; + foreach ($federatedShares as $federatedShare) { + if ($this->compareShare($userId, $federatedShare, $permission)) { + $higherPermissionShare = $federatedShare; + } + } + + // no shares are linked to current user. + if ($higherPermissionShare === null) { + return 1; + } + + try { + $storageKey = match ($higherPermissionShare->getShareType()) { + IShare::TYPE_REMOTE, IShare::TYPE_REMOTE_GROUP => $this->fileRequest->getFederatedShareStorageKey($higherPermissionShare, $instance), + IShare::TYPE_CIRCLE => $this->fileRequest->getTeamStorages($higherPermissionShare, $instance), + }; + } catch (UnhandledMatchError) { + return 1; + } + + if ($storageKey === null) { + return 1; + } + + // returns the id of the file at the end of the mount point + path to the file + $rootFileId = $this->fileRequest->getFilesFromExternalShareStorage($storageKey); + return $this->getFinalFileId($userId, $rootFileId, $higherPermissionShare->getTarget()); + } + + /** + * compare shares and extract the higher permission, + * confirm the link between current user and share. + * + * @return bool true if share has better permissions + */ + private function compareShare(string $userId, FederatedShare $federatedShare, int &$currentPermission): bool { + if ($federatedShare->getId() === 0 || $federatedShare->getShareWith() === '') { + return false; + } + + if ($currentPermission >= $federatedShare->getPermissions()) { + return false; + } + + if (($federatedShare->getShareType() === IShare::TYPE_REMOTE && $federatedShare->getShareWith() === $userId) + || ($federatedShare->getShareType() === IShare::TYPE_REMOTE_GROUP && in_array($federatedShare->getShareWith(), $this->getCurrentGroups($userId), true)) + || ($federatedShare->getShareType() === IShare::TYPE_CIRCLE && in_array($federatedShare->getShareWith(), $this->getCurrentTeams($userId), true))) { + $currentPermission = $federatedShare->getPermissions(); + return true; + } + + return false; + } + + /** + * Return the id of a local file based on the node id of the top + * folder / mount point and the path to reach the file + * + * @return int 1 if file not found + */ + private function getFinalFileId(string $user, int $nodeId, LocalFile $target): int { + try { + $userFolder = $this->rootFolder->getUserFolder($user); + } catch (NotPermittedException|NoUserException $e) { + $this->logger->debug('could not get final file id', ['exception' => $e]); + return 1; + } + + $folder = $userFolder->getFirstNodeById($nodeId); + if ($folder === null) { + return 1; + } + + foreach (array_reverse($target->getPath()) as $name) { + try { + $folder = $folder->get($name); + } catch (NotFoundException|NotPermittedException $e) { + $this->logger->debug('could not get final file id', ['exception' => $e]); + return 1; + } + } + + return $folder->getId(); + } + + /** + * request remote instance to get the list of federated shares between both instances that would + * provide access to file id search can also be performed on the share id. + * + * @return FederatedShare[] + * @throws LocalFederatedShareException if the federated share is not remote + */ + private function requestRemoteFederatedShares(string &$remote, array $search, bool $redirected = false): array { + if (str_contains($remote, '://')) { + $remote = parse_url($remote, PHP_URL_HOST); + } + + // this should not happen, but we keep a trace + if ($this->globalScaleService->isLocalAddress($remote)) { + $this->logger->warning('remote is local', ['exception' => new \Exception(), "remote" => $remote]); + return []; + } + + $responseCode = 0; + $result = $this->globalScaleService->requestGssOcs( + $remote, + 'Slave.sharedFile', + ['jwt' => JWT::encode(array_merge($search, ['instance' => $this->globalScaleService->getLocalAddress()]), $this->gss->getJwtKey(), Application::JWT_ALGORITHM)], + $responseCode); + + $this->logger->warning('result from remote gss ocs', ['remote' => $remote, 'search' => $search, 'data' => $result, 'responseCode' => $responseCode]); + + // in case file is not on remote instance, we get a redirection + if (!$redirected && $responseCode === Http::STATUS_MOVED_PERMANENTLY) { + $federatedShare = new FederatedShare(); + $federatedShare->import($result); + if (!$federatedShare->isBounce()) { + return []; + } + + // on redirection, we update &$remote + $remote = $federatedShare->getRemote(); + + /** + * in case of redirection (the file belongs to a different instance than the one that generates the internal-link), + * we check the new remote it is not current (local) instance. + * + * remoteId is the share id, so we extract the id of the shared file. + */ + if ($this->globalScaleService->isLocalAddress($remote)) { + throw new LocalFederatedShareException($federatedShare); + } + + return $this->requestRemoteFederatedShares($remote, ['shareId' => $federatedShare->getRemoteId(), 'target' => $federatedShare->getTarget()?->jsonSerialize() ?? []], true); + } + + if ($responseCode !== Http::STATUS_OK) { + return []; + } + + $federatedShares = []; + foreach($result as $entry) { + $federatedShare = new FederatedShare(); + $federatedShare->import($entry); + if (!$federatedShare->isBounce()) { + $federatedShares[] = $federatedShare; + } + } + + return $federatedShares; + } + + /** + * cache and returns list of current groups a userId belongs to + */ + private function getCurrentGroups(string $userId): array { + if (!array_key_exists($userId, $this->currentGroups)) { + $user = $this->userManager->get($userId); + if ($user === null) { + return []; + } + $this->currentGroups[$userId] = $this->groupManager->getUserGroupIds($user); + } + + return $this->currentGroups[$userId]; + } + + /** + * cache and returns list of current teams a userId belongs to + */ + private function getCurrentTeams(string $userId): array { + if (!array_key_exists($userId, $this->currentTeams)) { + $this->circlesManager->startSession($this->circlesManager->getLocalFederatedUser($userId)); + $teams = array_map(fn(Circle $team): string => $team->getSingleId(), $this->circlesManager->probeCircles()); + + $this->currentTeams[$userId] = $teams; + } + + return $this->currentTeams[$userId]; + } +} From 9b7f072af0438d9283573f23ad2f1b6091c94c2b Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Wed, 22 Oct 2025 18:06:02 -0100 Subject: [PATCH 2/3] fix tests Signed-off-by: Maxence Lange --- lib/Controller/SlaveController.php | 4 +- lib/Db/FileRequest.php | 92 +++++++++---------- lib/Db/ShareRequest.php | 58 ++++++------ .../LocalFederatedShareException.php | 4 +- lib/Exceptions/SharedFileException.php | 1 - lib/Master.php | 2 +- lib/Model/FederatedShare.php | 10 +- lib/Model/LocalFile.php | 8 +- lib/Service/GlobalScaleService.php | 76 +++++++-------- lib/Service/GlobalShareService.php | 82 ++++++++--------- .../lib/Controller/SlaveControllerTest.php | 8 ++ tests/unit/lib/LookupTest.php | 4 + 12 files changed, 180 insertions(+), 169 deletions(-) diff --git a/lib/Controller/SlaveController.php b/lib/Controller/SlaveController.php index e5889729..905c49f6 100644 --- a/lib/Controller/SlaveController.php +++ b/lib/Controller/SlaveController.php @@ -87,8 +87,8 @@ public function findFile(string $token, int $fileId): RedirectResponse { } /** - * return sharing details about a file. - * request must contain encoded jwt. + * return sharing details about a file. + * request must contain encoded jwt. */ #[PublicPage] #[NoCSRFRequired] diff --git a/lib/Db/FileRequest.php b/lib/Db/FileRequest.php index eccb311f..4eaaa12d 100644 --- a/lib/Db/FileRequest.php +++ b/lib/Db/FileRequest.php @@ -29,8 +29,8 @@ public function __construct( public function getFileDetails(int $fileId): ?LocalFile { $qb = $this->connection->getQueryBuilder(); $qb->select('parent', 'name', 'storage') - ->from('filecache') - ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + ->from('filecache') + ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); $result = $qb->executeQuery(); $row = $result->fetch(); @@ -39,9 +39,9 @@ public function getFileDetails(int $fileId): ?LocalFile { } $details = new LocalFile(); $details->setId($fileId) - ->setName($row['name'] ?? '') - ->setStorageId($row['storage'] ?? -1) - ->setParent($row['parent'] ?? -1); + ->setName($row['name'] ?? '') + ->setStorageId($row['storage'] ?? -1) + ->setParent($row['parent'] ?? -1); $result->closeCursor(); return $details; @@ -53,13 +53,13 @@ public function getFileDetails(int $fileId): ?LocalFile { public function getMountFromTarget(LocalFile $target): ?LocalMount { $qb = $this->connection->getQueryBuilder(); $qb->select('mount_provider_class', 'mount_point', 'user_id') - ->from('mounts') - ->where( - $qb->expr()->andX( - $qb->expr()->eq('storage_id', $qb->createNamedParameter($target->getStorageId(), IQueryBuilder::PARAM_INT)), - $qb->expr()->eq('root_id', $qb->createNamedParameter($target->getId(), IQueryBuilder::PARAM_INT)), - ) - ); + ->from('mounts') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('storage_id', $qb->createNamedParameter($target->getStorageId(), IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('root_id', $qb->createNamedParameter($target->getId(), IQueryBuilder::PARAM_INT)), + ) + ); $result = $qb->executeQuery(); $row = $result->fetch(); @@ -69,8 +69,8 @@ public function getMountFromTarget(LocalFile $target): ?LocalMount { $mount = new LocalMount(); $mount->setProviderClass($row['mount_provider_class']) - ->setMountPoint(rtrim(explode('/files', $row['mount_point'], 2)[1] ?? '', '/')) - ->setUserId($row['user_id']); + ->setMountPoint(rtrim(explode('/files', $row['mount_point'], 2)[1] ?? '', '/')) + ->setUserId($row['user_id']); $result->closeCursor(); @@ -83,11 +83,11 @@ public function getMountFromTarget(LocalFile $target): ?LocalMount { public function getFederatedTeamMount(LocalMount $mount, array $teamIds): ?FederatedShare { $qb = $this->connection->getQueryBuilder(); $qb->select('remote', 'remote_id') - ->from('circles_mount') - ->where( - $qb->expr()->eq('mountpoint_hash', $qb->createNamedParameter(md5($mount->getMountPoint()))), - $qb->expr()->in('circle_id', $qb->createNamedParameter($teamIds, IQueryBuilder::PARAM_STR_ARRAY)), - ); + ->from('circles_mount') + ->where( + $qb->expr()->eq('mountpoint_hash', $qb->createNamedParameter(md5($mount->getMountPoint()))), + $qb->expr()->in('circle_id', $qb->createNamedParameter($teamIds, IQueryBuilder::PARAM_STR_ARRAY)), + ); $result = $qb->executeQuery(); $row = $result->fetch(); @@ -97,8 +97,8 @@ public function getFederatedTeamMount(LocalMount $mount, array $teamIds): ?Feder $federatedShare = new FederatedShare(); $federatedShare->setRemote($row['remote']) - ->setRemoteId($row['remote_id']) - ->setBounce(true); + ->setRemoteId($row['remote_id']) + ->setBounce(true); $result->closeCursor(); @@ -111,15 +111,15 @@ public function getFederatedTeamMount(LocalMount $mount, array $teamIds): ?Feder public function getFilesFromExternalShareStorage(string $storageKey): int { $qb = $this->connection->getQueryBuilder(); $qb->select('c.fileid') - ->from('filecache', 'c') - ->from('storages', 's') - ->where( - $qb->expr()->andX( - $qb->expr()->eq('s.numeric_id', 'c.storage'), - $qb->expr()->eq('s.id', $qb->createNamedParameter($storageKey)), - $qb->expr()->eq('c.parent', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT)), - ) - ); + ->from('filecache', 'c') + ->from('storages', 's') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('s.numeric_id', 'c.storage'), + $qb->expr()->eq('s.id', $qb->createNamedParameter($storageKey)), + $qb->expr()->eq('c.parent', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT)), + ) + ); $result = $qb->executeQuery(); $row = $result->fetch(); @@ -137,14 +137,14 @@ public function getFilesFromExternalShareStorage(string $storageKey): int { public function getFederatedShareStorageKey(FederatedShare $federatedShare, string $instance): ?string { $qb = $this->connection->getQueryBuilder(); $qb->select('share_token', 'owner', 'remote') - ->from('share_external') - ->where( - $qb->expr()->andX( - $qb->expr()->like('remote', $qb->createNamedParameter('%://' . $instance . '/')), - $qb->expr()->eq('remote_id', $qb->createNamedParameter($federatedShare->getId(), IQueryBuilder::PARAM_INT)), - $qb->expr()->eq('user', $qb->createNamedParameter($federatedShare->getShareWith())) - ) - ); + ->from('share_external') + ->where( + $qb->expr()->andX( + $qb->expr()->like('remote', $qb->createNamedParameter('%://' . $instance . '/')), + $qb->expr()->eq('remote_id', $qb->createNamedParameter($federatedShare->getId(), IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('user', $qb->createNamedParameter($federatedShare->getShareWith())) + ) + ); $result = $qb->executeQuery(); $row = $result->fetch(); @@ -164,14 +164,14 @@ public function getFederatedShareStorageKey(FederatedShare $federatedShare, stri public function getTeamStorages(FederatedShare $federatedShare, string $instance): ?string { $qb = $this->connection->getQueryBuilder(); $qb->select('token', 'remote') - ->from('circles_mount') - ->where( - $qb->expr()->andX( - $qb->expr()->eq('remote', $qb->createNamedParameter($instance)), - $qb->expr()->eq('remote_id', $qb->createNamedParameter($federatedShare->getId(), IQueryBuilder::PARAM_INT)), - $qb->expr()->eq('circle_id', $qb->createNamedParameter($federatedShare->getShareWith())) - ) - ); + ->from('circles_mount') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('remote', $qb->createNamedParameter($instance)), + $qb->expr()->eq('remote_id', $qb->createNamedParameter($federatedShare->getId(), IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('circle_id', $qb->createNamedParameter($federatedShare->getShareWith())) + ) + ); $result = $qb->executeQuery(); $row = $result->fetch(); diff --git a/lib/Db/ShareRequest.php b/lib/Db/ShareRequest.php index 6d9bcc6d..9ca88d34 100644 --- a/lib/Db/ShareRequest.php +++ b/lib/Db/ShareRequest.php @@ -39,19 +39,19 @@ public function getFederatedSharesRelatedToRemoteInstance(array $files, string $ $qb = $this->connection->getQueryBuilder(); $qb->select('s.id', 's.file_source', 's.share_type', 's.share_with', 's.permissions') - ->from('share', 's') - ->where( - $qb->expr()->andX( - $qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)), - $qb->expr()->orX( - $qb->expr()->andX( - $qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_REMOTE, IShare::TYPE_REMOTE_GROUP], IQueryBuilder::PARAM_INT_ARRAY)), - $qb->expr()->like('share_with', $qb->createNamedParameter('%@' . $instance)), - ), - $qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_CIRCLE], IQueryBuilder::PARAM_INT_ARRAY)), - ) - ) - ); + ->from('share', 's') + ->where( + $qb->expr()->andX( + $qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)), + $qb->expr()->orX( + $qb->expr()->andX( + $qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_REMOTE, IShare::TYPE_REMOTE_GROUP], IQueryBuilder::PARAM_INT_ARRAY)), + $qb->expr()->like('share_with', $qb->createNamedParameter('%@' . $instance)), + ), + $qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_CIRCLE], IQueryBuilder::PARAM_INT_ARRAY)), + ) + ) + ); $result = $qb->executeQuery(); $shares = []; @@ -63,11 +63,11 @@ public function getFederatedSharesRelatedToRemoteInstance(array $files, string $ $federatedShare = new FederatedShare(); $federatedShare->setId($row['id']) - ->setFileId($row['file_source']) - ->setShareType($row['share_type']) - ->setShareWith($shareWith) - ->setPermissions($row['permissions']) - ->setTarget($indexedFiles[$row['file_source']]); + ->setFileId($row['file_source']) + ->setShareType($row['share_type']) + ->setShareWith($shareWith) + ->setPermissions($row['permissions']) + ->setTarget($indexedFiles[$row['file_source']]); $shares[] = $federatedShare; } $result->closeCursor(); @@ -83,8 +83,8 @@ public function getFederatedSharesRelatedToRemoteInstance(array $files, string $ public function getFileOwnerFromShareId(int $shareId): array { $qb = $this->connection->getQueryBuilder(); $qb->select('uid_owner', 'file_source') - ->from('share', 's') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId, IQueryBuilder::PARAM_INT))); + ->from('share', 's') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId, IQueryBuilder::PARAM_INT))); $result = $qb->executeQuery(); $row = $result->fetch(); @@ -105,13 +105,13 @@ public function getFileOwnerFromShareId(int $shareId): array { public function getBouncedShareFromLocalMount(LocalMount $mount): ?FederatedShare { $qb = $this->connection->getQueryBuilder(); $qb->select('remote', 'remote_id') - ->from('share_external') - ->where( - $qb->expr()->andX( - $qb->expr()->eq('user', $qb->createNamedParameter($mount->getUserId())), - $qb->expr()->eq('mountpoint_hash', $qb->createNamedParameter(md5($mount->getMountPoint()))), - ) - ); + ->from('share_external') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('user', $qb->createNamedParameter($mount->getUserId())), + $qb->expr()->eq('mountpoint_hash', $qb->createNamedParameter(md5($mount->getMountPoint()))), + ) + ); $result = $qb->executeQuery(); $row = $result->fetch(); @@ -120,8 +120,8 @@ public function getBouncedShareFromLocalMount(LocalMount $mount): ?FederatedShar } $bouncedShare = new FederatedShare(); $bouncedShare->setBounce(true) - ->setRemote($row['remote']) - ->setRemoteId((int)$row['remote_id']); + ->setRemote($row['remote']) + ->setRemoteId((int)$row['remote_id']); $result->closeCursor(); return $bouncedShare; diff --git a/lib/Exceptions/LocalFederatedShareException.php b/lib/Exceptions/LocalFederatedShareException.php index 174100d7..60324bec 100644 --- a/lib/Exceptions/LocalFederatedShareException.php +++ b/lib/Exceptions/LocalFederatedShareException.php @@ -15,9 +15,9 @@ class LocalFederatedShareException extends Exception { public function __construct( private readonly ?FederatedShare $federatedShare = null, - string $message = "", + string $message = '', int $code = 0, - Exception $previous = null, + ?Exception $previous = null, ) { parent::__construct($message, $code, $previous); } diff --git a/lib/Exceptions/SharedFileException.php b/lib/Exceptions/SharedFileException.php index eb94baac..e8579e7e 100644 --- a/lib/Exceptions/SharedFileException.php +++ b/lib/Exceptions/SharedFileException.php @@ -10,7 +10,6 @@ namespace OCA\GlobalSiteSelector\Exceptions; use Exception; -use OCA\GlobalSiteSelector\Model\FederatedShare; class SharedFileException extends Exception { } diff --git a/lib/Master.php b/lib/Master.php index 3975b804..a5524033 100644 --- a/lib/Master.php +++ b/lib/Master.php @@ -328,7 +328,7 @@ protected function getAppToken($location, $uid, $password, $options) { $data = json_decode($body, true); $jsonErrorCode = json_last_error(); if ($jsonErrorCode !== JSON_ERROR_NONE) { - $info = 'getAppToken - Decoding the JSON failed ' . $jsonErrorCode . ' '. json_last_error_msg(); + $info = 'getAppToken - Decoding the JSON failed ' . $jsonErrorCode . ' ' . json_last_error_msg(); throw new Exception($info); } if (!isset($data['ocs']['data']['token'])) { diff --git a/lib/Model/FederatedShare.php b/lib/Model/FederatedShare.php index 66d017d8..c6881e74 100644 --- a/lib/Model/FederatedShare.php +++ b/lib/Model/FederatedShare.php @@ -114,13 +114,13 @@ public function import(array $data): self { $this->setBounce($data['bounce'] ?? false); if ($this->isBounce()) { $this->setRemoteId($data['remoteId'] ?? 0) - ->setRemote($data['remote'] ?? ''); + ->setRemote($data['remote'] ?? ''); } else { $this->setId($data['id'] ?? 0) - ->setFileId($data['fileId'] ?? 0) - ->setShareType($data['shareType'] ?? 0) - ->setShareWith($data['shareWith'] ?? '') - ->setPermissions($data['permissions'] ?? 0); + ->setFileId($data['fileId'] ?? 0) + ->setShareType($data['shareType'] ?? 0) + ->setShareWith($data['shareWith'] ?? '') + ->setPermissions($data['permissions'] ?? 0); } if (array_key_exists('target', $data)) { diff --git a/lib/Model/LocalFile.php b/lib/Model/LocalFile.php index f7e43941..e59a8174 100644 --- a/lib/Model/LocalFile.php +++ b/lib/Model/LocalFile.php @@ -79,10 +79,10 @@ public function setPath(array $path): self { */ public function import(array $data): self { $this->setId($data['id'] ?? 0) - ->setName($data['name'] ?? '') - ->setStorageId($data['storageId'] ?? -1) - ->setParent($data['parent'] ?? -1) - ->setPath($data['path'] ?? []); + ->setName($data['name'] ?? '') + ->setStorageId($data['storageId'] ?? -1) + ->setParent($data['parent'] ?? -1) + ->setPath($data['path'] ?? []); return $this; } diff --git a/lib/Service/GlobalScaleService.php b/lib/Service/GlobalScaleService.php index 5bb1aff7..edc7d29c 100644 --- a/lib/Service/GlobalScaleService.php +++ b/lib/Service/GlobalScaleService.php @@ -34,10 +34,10 @@ public function __construct( ) { } - /** - * return local global scale identity token - * if none set yet, generate it - */ + /** + * return local global scale identity token + * if none set yet, generate it + */ public function getLocalToken(): string { if (!$this->appConfig->hasKey(Application::APP_ID, ConfigLexicon::LOCAL_TOKEN)) { $this->appConfig->setValueString(Application::APP_ID, ConfigLexicon::LOCAL_TOKEN, $this->secureRandom->generate(5, 'abcdefghijklmnopqrstuvwxyz0123456789')); @@ -46,23 +46,23 @@ public function getLocalToken(): string { return $this->appConfig->getValueString(Application::APP_ID, ConfigLexicon::LOCAL_TOKEN); } - /** - * return local address as known by lus - */ + /** + * return local address as known by lus + */ public function getLocalAddress(): ?string { return $this->getAddressFromToken($this->getLocalToken()); } - /** - * confirm a specific global scale token identify local instance - */ + /** + * confirm a specific global scale token identify local instance + */ public function isLocalToken(string $token): bool { return ($this->appConfig->getValueString(Application::APP_ID, ConfigLexicon::LOCAL_TOKEN) === $token); } - /** - * confirm that a url (or a host) is related to local instance - */ + /** + * confirm that a url (or a host) is related to local instance + */ public function isLocalAddress(string $address): bool { if (str_contains($address, '://')) { $address = parse_url($address, PHP_URL_HOST); @@ -84,7 +84,7 @@ public function refreshTokenFromGlobalScale(): void { } /** - * request global scale token from a remote instance using public discovery and store it in local cache + * request global scale token from a remote instance using public discovery and store it in local cache */ public function refreshTokenFromAddress(string $address): void { if (!$this->gss->isSlave()) { @@ -105,9 +105,9 @@ public function refreshTokenFromAddress(string $address): void { $this->appConfig->setValueArray(Application::APP_ID, ConfigLexicon::GS_TOKENS, $tokens); } - /** - * get address from a global scale token - */ + /** + * get address from a global scale token + */ public function getAddressFromToken(string $token): ?string { $tokens = $this->appConfig->getValueArray(Application::APP_ID, ConfigLexicon::GS_TOKENS); $address = array_search($token, $tokens, true); @@ -117,32 +117,32 @@ public function getAddressFromToken(string $token): ?string { return $address; } - /** - * returns global scale token from a specific address - */ + /** + * returns global scale token from a specific address + */ public function getTokenFromAddress(string $address): ?string { $tokens = $this->appConfig->getValueArray(Application::APP_ID, ConfigLexicon::GS_TOKENS); return $tokens[$address] ?? null; } - /** - * returns discovery data from a remote address - */ + /** + * returns discovery data from a remote address + */ public function getRemotePublicDiscovery(string $address): array { - return $this->requestGssOcs($address, 'Slave.discovery'); - } - - /** - * get data from a remote globalsiteselector ocs endpoint. - * - * @param string $address remote global scale instance - * @param string $route route name to the ocs endpoint - * @param array $data added to the request - * @param int $responseCode contains the response code from the request - * - * @return array decoded version of the json response - */ - public function requestGssOcs(string $address, string $route, array $data = [], int &$responseCode = 0): array { + return $this->requestGssOcs($address, 'Slave.discovery'); + } + + /** + * get data from a remote globalsiteselector ocs endpoint. + * + * @param string $address remote global scale instance + * @param string $route route name to the ocs endpoint + * @param array $data added to the request + * @param int $responseCode contains the response code from the request + * + * @return array decoded version of the json response + */ + public function requestGssOcs(string $address, string $route, array $data = [], int &$responseCode = 0): array { $client = $this->clientService->newClient(); try { $response = $client->get( @@ -159,7 +159,7 @@ public function requestGssOcs(string $address, string $route, array $data = [], } try { - $responseCode = $response->getStatusCode(); + $responseCode = $response->getStatusCode(); return json_decode($response->getBody(), true, flags: JSON_THROW_ON_ERROR)['ocs']['data'] ?? []; } catch (JsonException $e) { $this->logger->warning('could not decode json', ['exception' => $e]); diff --git a/lib/Service/GlobalShareService.php b/lib/Service/GlobalShareService.php index 5f859e28..175b192d 100644 --- a/lib/Service/GlobalShareService.php +++ b/lib/Service/GlobalShareService.php @@ -8,7 +8,7 @@ */ namespace OCA\GlobalSiteSelector\Service; -use OC\User\NoUserException; +use Exception; use OCA\Circles\CirclesManager; use OCA\Circles\Model\Circle; use OCA\Files_Sharing\External\MountProvider; @@ -39,8 +39,8 @@ class GlobalShareService { public function __construct( private readonly IRootFolder $rootFolder, private readonly IUserSession $userSession, - private readonly FileRequest $fileRequest, - private readonly ShareRequest $shareRequest, + private readonly FileRequest $fileRequest, + private readonly ShareRequest $shareRequest, private readonly GlobalSiteSelector $gss, private readonly GlobalScaleService $globalScaleService, private readonly IUserManager $userManager, @@ -58,7 +58,7 @@ public function getNewFileId(string $token, int $fileId): ?int { $currentUser = $this->userSession->getUser()?->getUID(); // There is no valid reason for getUser() to be null, if ($currentUser === null) { - $this->logger->warning('internal link request', ['exception' => new \Exception('could not assign current user')]); + $this->logger->warning('internal link request', ['exception' => new Exception('could not assign current user')]); return null; } @@ -142,15 +142,15 @@ public function getSharedFiles(int $fileId, int $shareId = 0, ?string $instance } - /** - * get details about a shared remote file based on the address of the remote - * instance and the id of the file as stored on that remote instance - * - * @param string $remote address of the remote instance - * @param int $remoteFileId id of the file as stored on the remote instance - * @return int local file id, 1 if not found - */ - private function getSharedFileRemoteDetails(string $remote, int $remoteFileId): int { + /** + * get details about a shared remote file based on the address of the remote + * instance and the id of the file as stored on that remote instance + * + * @param string $remote address of the remote instance + * @param int $remoteFileId id of the file as stored on the remote instance + * @return int local file id, 1 if not found + */ + private function getSharedFileRemoteDetails(string $remote, int $remoteFileId): int { $currentUser = $this->userSession->getUser()?->getUID(); if ($currentUser === null || $this->globalScaleService->getLocalAddress() === null) { return 1; @@ -174,11 +174,11 @@ private function getSharedFileRemoteDetails(string $remote, int $remoteFileId): return $this->getLastFileIdFromShares($currentUser, $federatedShares, $remote); } - /** - * returns details about a local file and (recursively) about all parent folders - * - * @return LocalFile[] - */ + /** + * returns details about a local file and (recursively) about all parent folders + * + * @return LocalFile[] + */ private function getRelatedFiles(int $fileId): array { if ($fileId === 0) { return []; @@ -307,16 +307,16 @@ private function compareShare(string $userId, FederatedShare $federatedShare, in return false; } - /** - * Return the id of a local file based on the node id of the top + /** + * Return the id of a local file based on the node id of the top * folder / mount point and the path to reach the file * * @return int 1 if file not found - */ + */ private function getFinalFileId(string $user, int $nodeId, LocalFile $target): int { try { $userFolder = $this->rootFolder->getUserFolder($user); - } catch (NotPermittedException|NoUserException $e) { + } catch (Exception $e) { $this->logger->debug('could not get final file id', ['exception' => $e]); return 1; } @@ -345,23 +345,23 @@ private function getFinalFileId(string $user, int $nodeId, LocalFile $target): i * @return FederatedShare[] * @throws LocalFederatedShareException if the federated share is not remote */ - private function requestRemoteFederatedShares(string &$remote, array $search, bool $redirected = false): array { - if (str_contains($remote, '://')) { - $remote = parse_url($remote, PHP_URL_HOST); - } + private function requestRemoteFederatedShares(string &$remote, array $search, bool $redirected = false): array { + if (str_contains($remote, '://')) { + $remote = parse_url($remote, PHP_URL_HOST); + } // this should not happen, but we keep a trace if ($this->globalScaleService->isLocalAddress($remote)) { - $this->logger->warning('remote is local', ['exception' => new \Exception(), "remote" => $remote]); + $this->logger->warning('remote is local', ['exception' => new Exception(), 'remote' => $remote]); return []; } $responseCode = 0; - $result = $this->globalScaleService->requestGssOcs( - $remote, - 'Slave.sharedFile', - ['jwt' => JWT::encode(array_merge($search, ['instance' => $this->globalScaleService->getLocalAddress()]), $this->gss->getJwtKey(), Application::JWT_ALGORITHM)], - $responseCode); + $result = $this->globalScaleService->requestGssOcs( + $remote, + 'Slave.sharedFile', + ['jwt' => JWT::encode(array_merge($search, ['instance' => $this->globalScaleService->getLocalAddress()]), $this->gss->getJwtKey(), Application::JWT_ALGORITHM)], + $responseCode); $this->logger->warning('result from remote gss ocs', ['remote' => $remote, 'search' => $search, 'data' => $result, 'responseCode' => $responseCode]); @@ -394,7 +394,7 @@ private function requestRemoteFederatedShares(string &$remote, array $search, bo } $federatedShares = []; - foreach($result as $entry) { + foreach ($result as $entry) { $federatedShare = new FederatedShare(); $federatedShare->import($entry); if (!$federatedShare->isBounce()) { @@ -403,11 +403,11 @@ private function requestRemoteFederatedShares(string &$remote, array $search, bo } return $federatedShares; - } + } - /** - * cache and returns list of current groups a userId belongs to - */ + /** + * cache and returns list of current groups a userId belongs to + */ private function getCurrentGroups(string $userId): array { if (!array_key_exists($userId, $this->currentGroups)) { $user = $this->userManager->get($userId); @@ -420,13 +420,13 @@ private function getCurrentGroups(string $userId): array { return $this->currentGroups[$userId]; } - /** - * cache and returns list of current teams a userId belongs to - */ + /** + * cache and returns list of current teams a userId belongs to + */ private function getCurrentTeams(string $userId): array { if (!array_key_exists($userId, $this->currentTeams)) { $this->circlesManager->startSession($this->circlesManager->getLocalFederatedUser($userId)); - $teams = array_map(fn(Circle $team): string => $team->getSingleId(), $this->circlesManager->probeCircles()); + $teams = array_map(fn (Circle $team): string => $team->getSingleId(), $this->circlesManager->probeCircles()); $this->currentTeams[$userId] = $teams; } diff --git a/tests/unit/lib/Controller/SlaveControllerTest.php b/tests/unit/lib/Controller/SlaveControllerTest.php index 5724ccbd..25f1fbff 100644 --- a/tests/unit/lib/Controller/SlaveControllerTest.php +++ b/tests/unit/lib/Controller/SlaveControllerTest.php @@ -11,6 +11,8 @@ use OCA\GlobalSiteSelector\AppInfo\Application; use OCA\GlobalSiteSelector\Controller\SlaveController; use OCA\GlobalSiteSelector\GlobalSiteSelector; +use OCA\GlobalSiteSelector\Service\GlobalScaleService; +use OCA\GlobalSiteSelector\Service\GlobalShareService; use OCA\GlobalSiteSelector\Service\SlaveService; use OCA\GlobalSiteSelector\TokenHandler; use OCA\GlobalSiteSelector\UserBackend; @@ -36,6 +38,8 @@ class SlaveControllerTest extends TestCase { private IUserManager $userManager; private UserBackend $userBackend; private ISession $session; + private GlobalScaleService $globalScaleService; + private GlobalShareService $globalShareService; private SlaveService $slaveService; private IConfig $config; @@ -56,6 +60,8 @@ public function setUp(): void { ->disableOriginalConstructor()->getMock(); $this->session = $this->createMock(ISession::class); $this->slaveService = $this->createMock(SlaveService::class); + $this->globalScaleService = $this->createMock(GlobalScaleService::class); + $this->globalShareService = $this->createMock(GlobalShareService::class); $this->config = $this->createMock(IConfig::class); } @@ -78,6 +84,8 @@ private function getInstance(array $mockMathods = []) { $this->userBackend, $this->session, $this->slaveService, + $this->globalScaleService, + $this->globalShareService, $this->config, $this->logger ] diff --git a/tests/unit/lib/LookupTest.php b/tests/unit/lib/LookupTest.php index 82c8ee63..b8b16818 100644 --- a/tests/unit/lib/LookupTest.php +++ b/tests/unit/lib/LookupTest.php @@ -8,6 +8,7 @@ namespace OCA\GlobalSiteSelector\Tests\Unit; +use OCA\GlobalSiteSelector\GlobalSiteSelector; use OCA\GlobalSiteSelector\Lookup; use OCP\Federation\ICloudId; use OCP\Federation\ICloudIdManager; @@ -21,6 +22,7 @@ class LookupTest extends TestCase { private IConfig $config; private LoggerInterface $logger; private ICloudIdManager $cloudIdManager; + private GlobalSiteSelector $gss; public function setUp(): void { parent::setUp(); @@ -28,6 +30,7 @@ public function setUp(): void { $this->httpClientService = $this->createMock(IClientService::class); $this->config = $this->createMock(IConfig::class); $this->logger = $this->createMock(LoggerInterface::class); + $this->gss = $this->createMock(GlobalSiteSelector::class); $this->cloudIdManager = $this->createMock(ICloudIdManager::class); } @@ -44,6 +47,7 @@ private function getInstance(array $mockMethods = []) { $this->httpClientService, $this->logger, $this->cloudIdManager, + $this->gss, $this->config ] )->onlyMethods($mockMethods)->getMock(); From f8cd96b465251fcfe88a9b9cbe6d7ca19f520cd3 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Tue, 13 Jan 2026 12:42:50 -0100 Subject: [PATCH 3/3] using IMountPointFileInfo Signed-off-by: Maxence Lange --- lib/Command/GlobalScaleDiscovery.php | 16 ++----- lib/Db/FileRequest.php | 71 ++++++++++++++++------------ 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/lib/Command/GlobalScaleDiscovery.php b/lib/Command/GlobalScaleDiscovery.php index 07754db0..05ada827 100644 --- a/lib/Command/GlobalScaleDiscovery.php +++ b/lib/Command/GlobalScaleDiscovery.php @@ -25,31 +25,21 @@ public function __construct( parent::__construct(); } - - /** - * - */ - protected function configure() { + protected function configure(): void { parent::configure(); $this->setName('globalsiteselector:discovery') ->addOption('current', '', InputOption::VALUE_NONE, 'display current data') ->setDescription('run a discovery request over Global Scale to get details about each instances'); } - /** - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int - */ protected function execute(InputInterface $input, OutputInterface $output): int { if ($input->getOption('current')) { $output->writeln(json_encode($this->appConfig->getValueArray(Application::APP_ID, ConfigLexicon::GS_TOKENS), JSON_PRETTY_PRINT)); - return 0; + return self::SUCCESS; } // currently, the only available data is a unique token that helps identify each instance $this->globalScaleService->refreshTokenFromGlobalScale(); - return 0; + return self::SUCCESS; } } diff --git a/lib/Db/FileRequest.php b/lib/Db/FileRequest.php index 4eaaa12d..367c4a75 100644 --- a/lib/Db/FileRequest.php +++ b/lib/Db/FileRequest.php @@ -9,17 +9,25 @@ namespace OCA\GlobalSiteSelector\Db; +use Exception; use OCA\GlobalSiteSelector\Model\FederatedShare; use OCA\GlobalSiteSelector\Model\LocalFile; use OCA\GlobalSiteSelector\Model\LocalMount; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Federation\ICloudIdManager; +use OCP\Files\Config\ICachedMountFileInfo; +use OCP\Files\Config\IUserMountCache; +use OCP\Files\IRootFolder; use OCP\IDBConnection; +use Psr\Log\LoggerInterface; class FileRequest { public function __construct( private readonly IDBConnection $connection, + private readonly IUserMountCache $userMountCache, + private readonly IRootFolder $rootFolder, private readonly ICloudIdManager $cloudIdManager, + private readonly LoggerInterface $logger, ) { } @@ -27,22 +35,27 @@ public function __construct( * return details from a local file id */ public function getFileDetails(int $fileId): ?LocalFile { - $qb = $this->connection->getQueryBuilder(); - $qb->select('parent', 'name', 'storage') - ->from('filecache') - ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + $cachedMount = $this->getCachedMountInfoFromNodeId($fileId); + if ($cachedMount === null) { + return null; + } - $result = $qb->executeQuery(); - $row = $result->fetch(); - if ($row === false) { + try { + $rootFolder = $this->rootFolder->getUserFolder($cachedMount->getUser()->getUID()); + } catch (Exception $e) { + $this->logger->warning('could not get root folder for user ' . $cachedMount->getUser()->getUID(), ['exception' => $e, 'fileId' => $fileId, 'userId' => $cachedMount->getUser()->getUID()]); + return null; + } + $node = $rootFolder->getFirstNodeById($fileId); + if ($node === null) { return null; } + $details = new LocalFile(); $details->setId($fileId) - ->setName($row['name'] ?? '') - ->setStorageId($row['storage'] ?? -1) - ->setParent($row['parent'] ?? -1); - $result->closeCursor(); + ->setName($node->getName()) + ->setStorageId($cachedMount->getStorageId()) + ->setParent($node->getParentId()); return $details; } @@ -51,28 +64,15 @@ public function getFileDetails(int $fileId): ?LocalFile { * return details about the mount point from a LocalFile */ public function getMountFromTarget(LocalFile $target): ?LocalMount { - $qb = $this->connection->getQueryBuilder(); - $qb->select('mount_provider_class', 'mount_point', 'user_id') - ->from('mounts') - ->where( - $qb->expr()->andX( - $qb->expr()->eq('storage_id', $qb->createNamedParameter($target->getStorageId(), IQueryBuilder::PARAM_INT)), - $qb->expr()->eq('root_id', $qb->createNamedParameter($target->getId(), IQueryBuilder::PARAM_INT)), - ) - ); - - $result = $qb->executeQuery(); - $row = $result->fetch(); - if ($row === false) { + $cachedMount = $this->getCachedMountInfoFromNodeId($target->getId()); + if ($cachedMount === null) { return null; } $mount = new LocalMount(); - $mount->setProviderClass($row['mount_provider_class']) - ->setMountPoint(rtrim(explode('/files', $row['mount_point'], 2)[1] ?? '', '/')) - ->setUserId($row['user_id']); - - $result->closeCursor(); + $mount->setProviderClass($cachedMount->getMountProvider()) + ->setMountPoint(rtrim(explode('/files', $cachedMount->getMountPoint(), 2)[1] ?? '', '/')) + ->setUserId($cachedMount->getUser()->getUID()); return $mount; } @@ -184,4 +184,17 @@ public function getTeamStorages(FederatedShare $federatedShare, string $instance return $storage; } + + /** + * returns the mount using the id of a node, + * userid can then be extracted and used to retrieve the file's root folder + */ + private function getCachedMountInfoFromNodeId(int $nodeId): ?ICachedMountFileInfo { + $mounts = $this->userMountCache->getMountsForFileId($nodeId); + if (empty($mounts ?? [])) { + $this->logger->warning('mount not found for node id ' . $nodeId); + } + + return reset($mounts); + } }