diff --git a/apps/federatedfilesharing/lib/FederatedShareProvider.php b/apps/federatedfilesharing/lib/FederatedShareProvider.php index 340de3a17611b..c9b15dc467c24 100644 --- a/apps/federatedfilesharing/lib/FederatedShareProvider.php +++ b/apps/federatedfilesharing/lib/FederatedShareProvider.php @@ -605,12 +605,7 @@ public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = t $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')); - $qb->andWhere($qb->expr()->eq('f.storage', $qb->createNamedParameter($node->getMountPoint()->getNumericStorageId(), IQueryBuilder::PARAM_INT))); - if ($shallow) { - $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId(), IQueryBuilder::PARAM_INT))); - } else { - $qb->andWhere($qb->expr()->like('f.path', $qb->createNamedParameter($this->dbConnection->escapeLikeParameter($node->getInternalPath()) . '/%'))); - } + $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); $qb->orderBy('id'); diff --git a/apps/files_sharing/lib/Updater.php b/apps/files_sharing/lib/Updater.php index 95a5c9d9166dc..8d3161ad8704f 100644 --- a/apps/files_sharing/lib/Updater.php +++ b/apps/files_sharing/lib/Updater.php @@ -26,9 +26,11 @@ */ namespace OCA\Files_Sharing; +use OC\Files\Cache\FileAccess; use OC\Files\Mount\MountPoint; use OCP\Constants; use OCP\Files\Folder; +use OCP\Server; use OCP\Share\IShare; class Updater { @@ -58,20 +60,40 @@ private static function moveShareInOrOutOfShare($path): void { if ($userFolder === null) { return; } + $user = $userFolder->getOwner(); + if (!$user) { + throw new \Exception("user folder has no owner"); + } $src = $userFolder->get($path); $shareManager = \OC::$server->getShareManager(); // FIXME: should CIRCLES be included here ?? - $shares = $shareManager->getSharesBy($userFolder->getOwner()->getUID(), IShare::TYPE_USER, $src, false, -1); - $shares = array_merge($shares, $shareManager->getSharesBy($userFolder->getOwner()->getUID(), IShare::TYPE_GROUP, $src, false, -1)); - $shares = array_merge($shares, $shareManager->getSharesBy($userFolder->getOwner()->getUID(), IShare::TYPE_ROOM, $src, false, -1)); + $shares = $shareManager->getSharesBy($user->getUID(), IShare::TYPE_USER, $src, false, -1); + $shares = array_merge($shares, $shareManager->getSharesBy($user->getUID(), IShare::TYPE_GROUP, $src, false, -1)); + $shares = array_merge($shares, $shareManager->getSharesBy($user->getUID(), IShare::TYPE_ROOM, $src, false, -1)); if ($src instanceof Folder) { - $subShares = $shareManager->getSharesInFolder($userFolder->getOwner()->getUID(), $src, false, false); + $cacheAccess = Server::get(FileAccess::class); + + $sourceStorageId = $src->getStorage()->getCache()->getNumericStorageId(); + $sourceInternalPath = $src->getInternalPath(); + $subShares = array_merge( + $shareManager->getSharesBy($user->getUID(), IShare::TYPE_USER), + $shareManager->getSharesBy($user->getUID(), IShare::TYPE_GROUP), + $shareManager->getSharesBy($user->getUID(), IShare::TYPE_ROOM), + ); + $shareSourceIds = array_map(fn (IShare $share) => $share->getNodeId(), $subShares); + $shareSources = $cacheAccess->getByFileIdsInStorage($shareSourceIds, $sourceStorageId); foreach ($subShares as $subShare) { - $shares = array_merge($shares, array_values($subShare)); + $shareCacheEntry = $shareSources[$subShare->getNodeId()] ?? null; + if ( + $shareCacheEntry && + str_starts_with($shareCacheEntry->getPath(), $sourceInternalPath . '/') + ) { + $shares[] = $subShare; + } } } diff --git a/apps/sharebymail/lib/ShareByMailProvider.php b/apps/sharebymail/lib/ShareByMailProvider.php index f1bb3fe94ca23..a219ef47dd3cf 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -1069,12 +1069,7 @@ public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = t $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')); - $qb->andWhere($qb->expr()->eq('f.storage', $qb->createNamedParameter($node->getMountPoint()->getNumericStorageId(), IQueryBuilder::PARAM_INT))); - if ($shallow) { - $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); - } else { - $qb->andWhere($qb->expr()->like('f.path', $qb->createNamedParameter($this->dbConnection->escapeLikeParameter($node->getInternalPath()) . '/%'))); - } + $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); $qb->orderBy('id'); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index b575228a7c2d9..b411839f4bd4b 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -317,6 +317,7 @@ 'OCP\\Files\\Cache\\CacheInsertEvent' => $baseDir . '/lib/public/Files/Cache/CacheInsertEvent.php', 'OCP\\Files\\Cache\\CacheUpdateEvent' => $baseDir . '/lib/public/Files/Cache/CacheUpdateEvent.php', 'OCP\\Files\\Cache\\ICache' => $baseDir . '/lib/public/Files/Cache/ICache.php', + 'OCP\\Files\\Cache\\ICacheAccess' => $baseDir . '/lib/public/Files/Cache/ICacheAccess.php', 'OCP\\Files\\Cache\\ICacheEntry' => $baseDir . '/lib/public/Files/Cache/ICacheEntry.php', 'OCP\\Files\\Cache\\ICacheEvent' => $baseDir . '/lib/public/Files/Cache/ICacheEvent.php', 'OCP\\Files\\Cache\\IPropagator' => $baseDir . '/lib/public/Files/Cache/IPropagator.php', @@ -1360,6 +1361,7 @@ 'OC\\Files\\AppData\\AppData' => $baseDir . '/lib/private/Files/AppData/AppData.php', 'OC\\Files\\AppData\\Factory' => $baseDir . '/lib/private/Files/AppData/Factory.php', 'OC\\Files\\Cache\\Cache' => $baseDir . '/lib/private/Files/Cache/Cache.php', + 'OC\\Files\\Cache\\CacheAccess' => $baseDir . '/lib/private/Files/Cache/CacheAccess.php', 'OC\\Files\\Cache\\CacheDependencies' => $baseDir . '/lib/private/Files/Cache/CacheDependencies.php', 'OC\\Files\\Cache\\CacheEntry' => $baseDir . '/lib/private/Files/Cache/CacheEntry.php', 'OC\\Files\\Cache\\CacheQueryBuilder' => $baseDir . '/lib/private/Files/Cache/CacheQueryBuilder.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index d03df46f06e65..756962daa018e 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -350,6 +350,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Files\\Cache\\CacheInsertEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Cache/CacheInsertEvent.php', 'OCP\\Files\\Cache\\CacheUpdateEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Cache/CacheUpdateEvent.php', 'OCP\\Files\\Cache\\ICache' => __DIR__ . '/../../..' . '/lib/public/Files/Cache/ICache.php', + 'OCP\\Files\\Cache\\ICacheAccess' => __DIR__ . '/../../..' . '/lib/public/Files/Cache/ICacheAccess.php', 'OCP\\Files\\Cache\\ICacheEntry' => __DIR__ . '/../../..' . '/lib/public/Files/Cache/ICacheEntry.php', 'OCP\\Files\\Cache\\ICacheEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Cache/ICacheEvent.php', 'OCP\\Files\\Cache\\IPropagator' => __DIR__ . '/../../..' . '/lib/public/Files/Cache/IPropagator.php', @@ -1393,6 +1394,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Files\\AppData\\AppData' => __DIR__ . '/../../..' . '/lib/private/Files/AppData/AppData.php', 'OC\\Files\\AppData\\Factory' => __DIR__ . '/../../..' . '/lib/private/Files/AppData/Factory.php', 'OC\\Files\\Cache\\Cache' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/Cache.php', + 'OC\\Files\\Cache\\CacheAccess' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/CacheAccess.php', 'OC\\Files\\Cache\\CacheDependencies' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/CacheDependencies.php', 'OC\\Files\\Cache\\CacheEntry' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/CacheEntry.php', 'OC\\Files\\Cache\\CacheQueryBuilder' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/CacheQueryBuilder.php', diff --git a/lib/private/Files/Cache/FileAccess.php b/lib/private/Files/Cache/FileAccess.php new file mode 100644 index 0000000000000..d5c1f81daf2af --- /dev/null +++ b/lib/private/Files/Cache/FileAccess.php @@ -0,0 +1,93 @@ +connection, + $this->systemConfig, + $this->logger, + $this->metadataManager, + ); + } + + public function getByFileIdInStorage(int $fileId, int $storageId): ?CacheEntry { + $items = $this->getByFileIdsInStorage([$fileId], $storageId); + return (count($items) > 0) ? $items[0] : null; + } + + public function getByPathInStorage(string $path, int $storageId): ?CacheEntry { + $query = $this->getQuery()->selectFileCache(); + $query->andWhere($query->expr()->eq('filecache.path_hash', $query->createNamedParameter(md5($path)))); + $query->andWhere($query->expr()->eq('filecache.storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))); + + $row = $query->executeQuery()->fetch(); + return $row ? Cache::cacheEntryFromData($row, $this->mimeTypeLoader) : null; + } + + public function getByFileId(int $fileId): ?CacheEntry { + $items = $this->getByFileIds([$fileId]); + return (count($items) > 0) ? $items[0] : null; + } + + /** + * @param array[] $rows + * @return array + */ + private function rowsToEntries(array $rows): array { + $result = []; + foreach ($rows as $row) { + $entry = Cache::cacheEntryFromData($row, $this->mimeTypeLoader); + $result[$entry->getId()] = $entry; + } + return $result; + } + + /** + * @param int[] $fileIds + * @return array + */ + public function getByFileIds(array $fileIds): array { + $query = $this->getQuery()->selectFileCache(); + $query->andWhere($query->expr()->in('filecache.fileid', $query->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY))); + + $rows = $query->executeQuery()->fetchAll(); + return $this->rowsToEntries($rows); + } + + /** + * @param int[] $fileIds + * @param int $storageId + * @return array + */ + public function getByFileIdsInStorage(array $fileIds, int $storageId): array { + $fileIds = array_values($fileIds); + $query = $this->getQuery()->selectFileCache(); + $query->andWhere($query->expr()->in('filecache.fileid', $query->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY))); + $query->andWhere($query->expr()->eq('filecache.storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))); + + $rows = $query->executeQuery()->fetchAll(); + return $this->rowsToEntries($rows); + } +} diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php index 8275eee7b9fe4..0f4b081bf20ce 100644 --- a/lib/private/Files/Config/UserMountCache.php +++ b/lib/private/Files/Config/UserMountCache.php @@ -26,8 +26,10 @@ * along with this program. If not, see * */ + namespace OC\Files\Config; +use OC\Files\Cache\FileAccess; use OC\User\LazyUser; use OCP\Cache\CappedMemoryCache; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -45,37 +47,31 @@ * Cache mounts points per user in the cache so we can easily look them up */ class UserMountCache implements IUserMountCache { - private IDBConnection $connection; - private IUserManager $userManager; - /** * Cached mount info. + * * @var CappedMemoryCache **/ private CappedMemoryCache $mountsForUsers; /** * fileid => internal path mapping for cached mount info. + * * @var CappedMemoryCache **/ private CappedMemoryCache $internalPathCache; - private LoggerInterface $logger; /** @var CappedMemoryCache */ private CappedMemoryCache $cacheInfoCache; - private IEventLogger $eventLogger; /** * UserMountCache constructor. */ public function __construct( - IDBConnection $connection, - IUserManager $userManager, - LoggerInterface $logger, - IEventLogger $eventLogger + private IDBConnection $connection, + private IUserManager $userManager, + private LoggerInterface $logger, + private IEventLogger $eventLogger, + private FileAccess $cacheAccess, ) { - $this->connection = $connection; - $this->userManager = $userManager; - $this->logger = $logger; - $this->eventLogger = $eventLogger; $this->cacheInfoCache = new CappedMemoryCache(); $this->internalPathCache = new CappedMemoryCache(); $this->mountsForUsers = new CappedMemoryCache(); @@ -282,11 +278,8 @@ public function getInternalPathForMountInfo(CachedMountInfo $info): string { if ($cached !== null) { return $cached; } - $builder = $this->connection->getQueryBuilder(); - $query = $builder->select('path') - ->from('filecache') - ->where($builder->expr()->eq('fileid', $builder->createPositionalParameter($info->getRootId()))); - return $query->executeQuery()->fetchOne() ?: ''; + $entry = $this->cacheAccess->getByFileIdInStorage($info->getRootId(), $info->getStorageId()); + return $entry ? $entry->getPath() : ''; } /** @@ -294,11 +287,10 @@ public function getInternalPathForMountInfo(CachedMountInfo $info): string { * @param string|null $user limit the results to a single user * @return CachedMountInfo[] */ - public function getMountsForStorageId($numericStorageId, $user = null) { + public function getMountsForStorageId($numericStorageId, $user = null, bool $preloadPaths = false) { $builder = $this->connection->getQueryBuilder(); - $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class') + $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'mount_provider_class') ->from('mounts', 'm') - ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) ->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT))); if ($user) { @@ -309,7 +301,21 @@ public function getMountsForStorageId($numericStorageId, $user = null) { $rows = $result->fetchAll(); $result->closeCursor(); - return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows)); + if ($preloadPaths) { + $fileIds = array_map(fn (array $row) => $row['root_id'], $rows); + $files = $this->cacheAccess->getByFileIds($fileIds); + + foreach ($rows as &$row) { + $mountFileId = $row['root_id']; + if (isset($files[$mountFileId])) { + $row['path'] = $files[$mountFileId]->getPath(); + } + } + } + + return array_filter(array_map(function (array $row) use ($preloadPaths) { + return $this->dbRowToMountInfo($row, $preloadPaths ? null : [$this, 'getInternalPathForMountInfo']); + }, $rows)); } /** @@ -318,45 +324,17 @@ public function getMountsForStorageId($numericStorageId, $user = null) { */ public function getMountsForRootId($rootFileId) { $builder = $this->connection->getQueryBuilder(); - $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class') + $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'mount_provider_class') ->from('mounts', 'm') - ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) ->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT))); $result = $query->execute(); $rows = $result->fetchAll(); $result->closeCursor(); - return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows)); - } - - /** - * @param $fileId - * @return array{int, string, int} - * @throws \OCP\Files\NotFoundException - */ - private function getCacheInfoFromFileId($fileId): array { - if (!isset($this->cacheInfoCache[$fileId])) { - $builder = $this->connection->getQueryBuilder(); - $query = $builder->select('storage', 'path', 'mimetype') - ->from('filecache') - ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); - - $result = $query->execute(); - $row = $result->fetch(); - $result->closeCursor(); - - if (is_array($row)) { - $this->cacheInfoCache[$fileId] = [ - (int)$row['storage'], - (string)$row['path'], - (int)$row['mimetype'] - ]; - } else { - throw new NotFoundException('File with id "' . $fileId . '" not found'); - } - } - return $this->cacheInfoCache[$fileId]; + return array_filter(array_map(function (array $row) { + return $this->dbRowToMountInfo($row, [$this, 'getInternalPathForMountInfo']); + }, $rows)); } /** @@ -366,12 +344,13 @@ private function getCacheInfoFromFileId($fileId): array { * @since 9.0.0 */ public function getMountsForFileId($fileId, $user = null) { - try { - [$storageId, $internalPath] = $this->getCacheInfoFromFileId($fileId); - } catch (NotFoundException $e) { + $cacheEntry = $this->cacheAccess->getByFileId($fileId); + if (!$cacheEntry) { return []; } - $mountsForStorage = $this->getMountsForStorageId($storageId, $user); + $internalPath = $cacheEntry->getPath(); + + $mountsForStorage = $this->getMountsForStorageId($cacheEntry->getStorageId(), $user, true); // filter mounts that are from the same storage but not a parent of the file we care about $filteredMounts = array_filter($mountsForStorage, function (ICachedMountInfo $mount) use ($internalPath, $fileId) { @@ -449,13 +428,8 @@ public function getUsedSpaceForUsers(array $users) { return $user->getUID(); }, $users); - $query = $builder->select('m.user_id', 'f.size') + $query = $builder->select('m.user_id', 'storage_id') ->from('mounts', 'm') - ->innerJoin('m', 'filecache', 'f', - $builder->expr()->andX( - $builder->expr()->eq('m.storage_id', 'f.storage'), - $builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files'))) - )) ->where($builder->expr()->eq('m.mount_point', $mountPoint)) ->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY))); @@ -463,12 +437,17 @@ public function getUsedSpaceForUsers(array $users) { $results = []; while ($row = $result->fetch()) { - $results[$row['user_id']] = $row['size']; + $results[$row['user_id']] = $this->getSizeForHomeStorage($row['storage_id']); } $result->closeCursor(); return $results; } + private function getSizeForHomeStorage(int $storage): int { + $entry = $this->cacheAccess->getByPathInStorage('files', $storage); + return $entry->getSize(); + } + public function clear(): void { $this->cacheInfoCache = new CappedMemoryCache(); $this->mountsForUsers = new CappedMemoryCache(); diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index c22c65f6c7b44..fd046e8179145 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -35,6 +35,8 @@ namespace OC\Share20; use OC\Files\Cache\Cache; +use OC\Files\Cache\FileAccess; +use OC\Files\Cache\CacheEntry; use OC\Share20\Exception\BackendError; use OC\Share20\Exception\InvalidShare; use OC\Share20\Exception\ProviderException; @@ -67,52 +69,21 @@ class DefaultShareProvider implements IShareProvider { // Special share type for user modified group shares public const SHARE_TYPE_USERGROUP = 2; - /** @var IDBConnection */ - private $dbConn; - - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var IMailer */ - private $mailer; - - /** @var Defaults */ - private $defaults; - - /** @var IFactory */ - private $l10nFactory; - - /** @var IURLGenerator */ - private $urlGenerator; - - private ITimeFactory $timeFactory; + private IDBConnection $dbConn; public function __construct( - IDBConnection $connection, - IUserManager $userManager, - IGroupManager $groupManager, - IRootFolder $rootFolder, - IMailer $mailer, - Defaults $defaults, - IFactory $l10nFactory, - IURLGenerator $urlGenerator, - ITimeFactory $timeFactory, + IDBConnection $connection, + private IUserManager $userManager, + private IGroupManager $groupManager, + private IRootFolder $rootFolder, + private IMailer $mailer, + private Defaults $defaults, + private IFactory $l10nFactory, + private IURLGenerator $urlGenerator, + private ITimeFactory $timeFactory, + private FileAccess $cacheAccess, ) { $this->dbConn = $connection; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->rootFolder = $rootFolder; - $this->mailer = $mailer; - $this->defaults = $defaults; - $this->l10nFactory = $l10nFactory; - $this->urlGenerator = $urlGenerator; - $this->timeFactory = $timeFactory; } /** @@ -653,10 +624,7 @@ public function move(\OCP\Share\IShare $share, $recipient) { public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = true) { $qb = $this->dbConn->getQueryBuilder(); - $qb->select('s.*', - 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', - 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', - 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum') + $qb->select('s.*') ->from('share', 's') ->andWhere($qb->expr()->orX( $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), @@ -683,43 +651,17 @@ public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = t ); } - // todo? maybe get these from the oc_mounts table - $childMountNodes = array_filter($node->getDirectoryListing(), function (Node $node): bool { - return $node->getInternalPath() === ''; - }); - $childMountRootIds = array_map(function (Node $node): int { + $childFileIds = array_map(function (Node $node): int { return $node->getId(); - }, $childMountNodes); + }, $node->getDirectoryListing()); - $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')); - $storageFilter = $qb->expr()->eq('f.storage', $qb->createNamedParameter($node->getMountPoint()->getNumericStorageId(), IQueryBuilder::PARAM_INT)); - if ($shallow) { - $qb->andWhere( - $qb->expr()->orX( - $qb->expr()->andX( - $storageFilter, - $qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())), - ), - $qb->expr()->in('f.fileid', $qb->createParameter('chunk')) - ) - ); - } else { - $qb->andWhere( - $qb->expr()->orX( - $qb->expr()->andX( - $storageFilter, - $qb->expr()->like('f.path', $qb->createNamedParameter($this->dbConn->escapeLikeParameter($node->getInternalPath()) . '/%')), - ), - $qb->expr()->in('f.fileid', $qb->createParameter('chunk')) - ) - ); - } + $qb->andWhere($qb->expr()->in('s.file_source', $qb->createParameter('chunk'))); $qb->orderBy('id'); $shares = []; - $chunks = array_chunk($childMountRootIds, 1000); + $chunks = array_chunk($childFileIds, 1000); // Force the request to be run when there is 0 mount. if (count($chunks) === 0) { @@ -730,7 +672,7 @@ public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = t $qb->setParameter('chunk', $chunk, IQueryBuilder::PARAM_INT_ARRAY); $cursor = $qb->executeQuery(); while ($data = $cursor->fetch()) { - $shares[$data['fileid']][] = $this->createShare($data); + $shares[$data['file_source']][] = $this->createShare($data); } $cursor->closeCursor(); } @@ -872,25 +814,9 @@ public function getSharesByPath(Node $path) { * Returns whether the given database result can be interpreted as * a share with accessible file (not trashed, not deleted) */ - private function isAccessibleResult($data) { - // exclude shares leading to deleted file entries - if ($data['fileid'] === null || $data['path'] === null) { - return false; - } - - // exclude shares leading to trashbin on home storages - $pathSections = explode('/', $data['path'], 2); - // FIXME: would not detect rare md5'd home storage case properly - if ($pathSections[0] !== 'files' - && (str_starts_with($data['storage_string_id'], 'home::') || str_starts_with($data['storage_string_id'], 'object::user'))) { - return false; - } elseif ($pathSections[0] === '__groupfolders' - && str_starts_with($pathSections[1], 'trash/') - ) { - // exclude shares leading to trashbin on group folders storages - return false; - } - return true; + private function isAccessibleResult(CacheEntry $data) { + $path = $data->getPath(); + return !(str_starts_with($path, 'files_trashbin/') || str_starts_with($path, '__groupfolders/trash/')); } /** @@ -903,15 +829,8 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset) { if ($shareType === IShare::TYPE_USER) { //Get shares directly with this user $qb = $this->dbConn->getQueryBuilder(); - $qb->select('s.*', - 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', - 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', - 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum' - ) - ->selectAlias('st.id', 'storage_string_id') - ->from('share', 's') - ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')) - ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id')); + $qb->select('s.*') + ->from('share', 's'); // Order by id $qb->orderBy('s.id'); @@ -937,14 +856,7 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset) { $cursor = $qb->execute(); while ($data = $cursor->fetch()) { - if ($data['fileid'] && $data['path'] === null) { - $data['path'] = (string) $data['path']; - $data['name'] = (string) $data['name']; - $data['checksum'] = (string) $data['checksum']; - } - if ($this->isAccessibleResult($data)) { - $shares[] = $this->createShare($data); - } + $shares[] = $this->createShare($data); } $cursor->closeCursor(); } elseif ($shareType === IShare::TYPE_GROUP) { @@ -964,15 +876,8 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset) { } $qb = $this->dbConn->getQueryBuilder(); - $qb->select('s.*', - 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', - 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', - 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum' - ) - ->selectAlias('st.id', 'storage_string_id') + $qb->select('s.*') ->from('share', 's') - ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')) - ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id')) ->orderBy('s.id') ->setFirstResult(0); @@ -1005,10 +910,8 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset) { continue; } - if ($this->isAccessibleResult($data)) { - $share = $this->createShare($data); - $shares2[$share->getId()] = $share; - } + $share = $this->createShare($data); + $shares2[$share->getId()] = $share; } $cursor->closeCursor(); } @@ -1022,7 +925,31 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset) { } - return $shares; + return $this->setNodes($shares); + } + + /** + * @param IShare[] $shares + * @return IShare[] + */ + private function setNodes(array $shares): array { + $fileIds = array_map(function (IShare $share): int { + return $share->getNodeId(); + }, $shares); + $files = $this->cacheAccess->getByFileIds($fileIds); + + $sharesWithFiles = []; + foreach ($shares as $share) { + if (isset($files[$share->getNodeId()])) { + $cacheItem = $files[$share->getNodeId()]; + if ($this->isAccessibleResult($cacheItem)) { + $share->setNodeCacheEntry($cacheItem); + $sharesWithFiles[] = $share; + } + } + } + + return $sharesWithFiles; } /** diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 2bef6ba3ce7bf..575cc0b5d6a4d 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -1317,9 +1317,12 @@ public function moveShare(IShare $share, $recipientId) { public function getSharesInFolder($userId, Folder $node, $reshares = false, $shallow = true) { $providers = $this->factory->getAllProviders(); + if (!$shallow) { + throw new \Exception("non-shallow getSharesInFolder is no longer supported"); + } return array_reduce($providers, function ($shares, IShareProvider $provider) use ($userId, $node, $reshares, $shallow) { - $newShares = $provider->getSharesInFolder($userId, $node, $reshares, $shallow); + $newShares = $provider->getSharesInFolder($userId, $node, $reshares); foreach ($newShares as $fid => $data) { if (!isset($shares[$fid])) { $shares[$fid] = []; diff --git a/lib/private/Share20/ProviderFactory.php b/lib/private/Share20/ProviderFactory.php index cac25226f1ae9..99bd8d52b4819 100644 --- a/lib/private/Share20/ProviderFactory.php +++ b/lib/private/Share20/ProviderFactory.php @@ -41,7 +41,6 @@ use OCA\ShareByMail\Settings\SettingsManager; use OCA\ShareByMail\ShareByMailProvider; use OCA\Talk\Share\RoomShareProvider; -use OCP\AppFramework\Utility\ITimeFactory; use OCP\Defaults; use OCP\EventDispatcher\IEventDispatcher; use OCP\Federation\ICloudFederationFactory; @@ -99,17 +98,7 @@ public function registerProvider(string $shareProviderClass): void { */ protected function defaultShareProvider() { if ($this->defaultProvider === null) { - $this->defaultProvider = new DefaultShareProvider( - $this->serverContainer->getDatabaseConnection(), - $this->serverContainer->getUserManager(), - $this->serverContainer->getGroupManager(), - $this->serverContainer->get(IRootFolder::class), - $this->serverContainer->getMailer(), - $this->serverContainer->query(Defaults::class), - $this->serverContainer->getL10NFactory(), - $this->serverContainer->getURLGenerator(), - $this->serverContainer->query(ITimeFactory::class), - ); + $this->defaultProvider = $this->serverContainer->get(DefaultShareProvider::class); } return $this->defaultProvider; diff --git a/lib/public/Files/Cache/IFileAccess.php b/lib/public/Files/Cache/IFileAccess.php new file mode 100644 index 0000000000000..c3c3f246b2ace --- /dev/null +++ b/lib/public/Files/Cache/IFileAccess.php @@ -0,0 +1,76 @@ + + * + * @since 29.0.0 + */ + public function getByFileIds(array $fileIds): array; + + /** + * Get filecache data by file ids from a specific storage. + * + * This is prefered over `getByFileIds` when the storage id is known as it + * can be more efficient in some setups. + * + * @param int[] $fileIds + * @param int $storageId + * @return array + * + * @since 29.0.0 + */ + public function getByFileIdsInStorage(array $fileIds, int $storageId): array; +} diff --git a/tests/lib/Files/Config/UserMountCacheTest.php b/tests/lib/Files/Config/UserMountCacheTest.php index 9e910f4f47f5b..ff83254af29f3 100644 --- a/tests/lib/Files/Config/UserMountCacheTest.php +++ b/tests/lib/Files/Config/UserMountCacheTest.php @@ -8,11 +8,14 @@ namespace Test\Files\Config; +use OC\DB\Exceptions\DbalException; use OC\DB\QueryBuilder\Literal; +use OC\Files\Cache\FileAccess; use OC\Files\Mount\MountPoint; use OC\Files\Storage\Storage; use OC\User\Manager; use OCP\Cache\CappedMemoryCache; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\ICachedMountInfo; @@ -20,6 +23,7 @@ use OCP\IConfig; use OCP\IDBConnection; use OCP\IUserManager; +use OCP\Server; use Psr\Log\LoggerInterface; use Test\TestCase; use Test\Util\User\Dummy; @@ -28,10 +32,8 @@ * @group DB */ class UserMountCacheTest extends TestCase { - /** - * @var IDBConnection - */ - private $connection; + private IDBConnection $connection; + private FileAccess $cacheAccess; /** * @var IUserManager @@ -49,7 +51,8 @@ protected function setUp(): void { parent::setUp(); $this->fileIds = []; - $this->connection = \OC::$server->getDatabaseConnection(); + $this->connection = Server::get(IDBConnection::class); + $this->cacheAccess = Server::get(FileAccess::class); $config = $this->getMockBuilder(IConfig::class) ->disableOriginalConstructor() ->getMock(); @@ -67,7 +70,13 @@ protected function setUp(): void { $userBackend->createUser('u2', ''); $userBackend->createUser('u3', ''); $this->userManager->registerBackend($userBackend); - $this->cache = new \OC\Files\Config\UserMountCache($this->connection, $this->userManager, $this->createMock(LoggerInterface::class), $this->createMock(IEventLogger::class)); + $this->cache = new \OC\Files\Config\UserMountCache( + $this->connection, + $this->userManager, + $this->createMock(LoggerInterface::class), + $this->createMock(IEventLogger::class), + $this->cacheAccess, + ); } protected function tearDown(): void { @@ -336,31 +345,36 @@ private function sortMounts(&$mounts) { private function createCacheEntry($internalPath, $storageId, $size = 0) { $internalPath = trim($internalPath, '/'); - $inserted = $this->connection->insertIfNotExist('*PREFIX*filecache', [ - 'storage' => $storageId, - 'path' => $internalPath, - 'path_hash' => md5($internalPath), - 'parent' => -1, - 'name' => basename($internalPath), - 'mimetype' => 0, - 'mimepart' => 0, - 'size' => $size, - 'storage_mtime' => 0, - 'encrypted' => 0, - 'unencrypted_size' => 0, - 'etag' => '', - 'permissions' => 31 - ], ['storage', 'path_hash']); - if ($inserted) { - $id = (int)$this->connection->lastInsertId('*PREFIX*filecache'); + $query = $this->connection->getQueryBuilder(); + $query->insert('filecache') + ->values([ + 'storage' => $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT), + 'path' => $query->createNamedParameter($internalPath), + 'path_hash' => $query->createNamedParameter(md5($internalPath)), + 'parent' => $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT), + 'name' => $query->createNamedParameter(basename($internalPath)), + 'mimetype' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), + 'mimepart' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), + 'size' => $query->createNamedParameter($size, IQueryBuilder::PARAM_INT), + 'storage_mtime' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), + 'encrypted' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), + 'unencrypted_size' => $query->createNamedParameter(0, IQueryBuilder::PARAM_INT), + 'etag' => $query->createNamedParameter(''), + 'permissions' => $query->createNamedParameter(31, IQueryBuilder::PARAM_INT), + ]); + try { + $query->executeStatement(); + $id = (int)$query->getLastInsertId(); $this->fileIds[] = $id; - } else { - $sql = 'SELECT `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path_hash` =?'; - $query = $this->connection->prepare($sql); - $query->execute([$storageId, md5($internalPath)]); - return (int)$query->fetchOne(); + return $id; + } catch (DbalException $e) { + $query = $this->connection->getQueryBuilder(); + $query->select('fileid') + ->from('filecache') + ->where($query->expr()->eq('storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('path_hash', $query->createNamedParameter(md5($internalPath)))); + return (int)$query->executeQuery()->fetchOne(); } - return $id; } public function testGetMountsForFileIdRootId() { diff --git a/tests/lib/Share20/DefaultShareProviderTest.php b/tests/lib/Share20/DefaultShareProviderTest.php index 315ee66bb3180..f1015bc79858d 100644 --- a/tests/lib/Share20/DefaultShareProviderTest.php +++ b/tests/lib/Share20/DefaultShareProviderTest.php @@ -22,6 +22,8 @@ namespace Test\Share20; +use OC\Files\Cache\FileAccess; +use OC\Files\Cache\CacheEntry; use OC\Share20\DefaultShareProvider; use OC\Share20\ShareAttributes; use OCP\AppFramework\Utility\ITimeFactory; @@ -83,6 +85,11 @@ class DefaultShareProviderTest extends \Test\TestCase { /** @var ITimeFactory|MockObject */ protected $timeFactory; + /** @var FileAccess|MockObject */ + protected $cacheAccess; + /** @var array */ + protected $cacheItems = []; + protected function setUp(): void { $this->dbConn = \OC::$server->getDatabaseConnection(); $this->userManager = $this->createMock(IUserManager::class); @@ -94,6 +101,26 @@ protected function setUp(): void { $this->defaults = $this->getMockBuilder(Defaults::class)->disableOriginalConstructor()->getMock(); $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->cacheAccess = $this->createMock(FileAccess::class); + $this->cacheItems = []; + $this->cacheAccess->method('getByFileIds')->willReturnCallback(function (array $fileIds) { + $result = []; + foreach ($fileIds as $fileId) { + if (isset($this->cacheItems[$fileId])) { + $result[$fileId] = $this->cacheItems[$fileId]; + } + } + return $result; + }); + $this->cacheAccess->method('getByFileIdsInStorage')->willReturnCallback(function (array $fileIds, int $storageId) { + $result = []; + foreach ($fileIds as $fileId) { + if (isset($this->cacheItems[$fileId]) && $this->cacheItems[$fileId]->getStorageId() === $storageId) { + $result[$fileId] = $this->cacheItems[$fileId]; + } + } + return $result; + }); $this->userManager->expects($this->any())->method('userExists')->willReturn(true); $this->timeFactory->expects($this->any())->method('now')->willReturn(new \DateTimeImmutable("2023-05-04 00:00 Europe/Berlin")); @@ -110,13 +137,13 @@ protected function setUp(): void { $this->defaults, $this->l10nFactory, $this->urlGenerator, - $this->timeFactory + $this->timeFactory, + $this->cacheAccess, ); } protected function tearDown(): void { $this->dbConn->getQueryBuilder()->delete('share')->execute(); - $this->dbConn->getQueryBuilder()->delete('filecache')->execute(); $this->dbConn->getQueryBuilder()->delete('storages')->execute(); } @@ -471,7 +498,8 @@ public function testDeleteSingleShare() { $this->defaults, $this->l10nFactory, $this->urlGenerator, - $this->timeFactory + $this->timeFactory, + $this->cacheAccess, ]) ->setMethods(['getShareById']) ->getMock(); @@ -566,7 +594,8 @@ public function testDeleteGroupShareWithUserGroupShares() { $this->defaults, $this->l10nFactory, $this->urlGenerator, - $this->timeFactory + $this->timeFactory, + $this->cacheAccess, ]) ->setMethods(['getShareById']) ->getMock(); @@ -921,16 +950,15 @@ private function createTestStorageEntry($storageStringId) { } private function createTestFileEntry($path, $storage = 1) { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('filecache') - ->values([ - 'storage' => $qb->expr()->literal($storage), - 'path' => $qb->expr()->literal($path), - 'path_hash' => $qb->expr()->literal(md5($path)), - 'name' => $qb->expr()->literal(basename($path)), - ]); - $this->assertEquals(1, $qb->execute()); - return $qb->getLastInsertId(); + $id = count($this->cacheItems); + $this->cacheItems[$id] = new CacheEntry([ + 'fileid' => $id, + 'storage' => $storage, + 'path' => $path, + 'path_hash' => md5($path), + 'name' => basename($path), + ]); + return $id; } public function storageAndFileNameProvider() { @@ -939,8 +967,6 @@ public function storageAndFileNameProvider() { ['home::shareOwner', 'files/test.txt', 'files/test2.txt'], // regular file on external storage ['smb::whatever', 'files/test.txt', 'files/test2.txt'], - // regular file on external storage in trashbin-like folder, - ['smb::whatever', 'files_trashbin/files/test.txt', 'files_trashbin/files/test2.txt'], ]; } @@ -1288,6 +1314,7 @@ public function testGetSharedWithWithDeletedFile($shareType, $trashed) { $user = $this->createMock(IUser::class); $user->method('getUID')->willReturn('sharedWith'); + $user->method('getDisplayName')->willReturn('sharedWith'); $owner = $this->createMock(IUser::class); $owner->method('getUID')->willReturn('shareOwner'); $initiator = $this->createMock(IUser::class); @@ -2526,7 +2553,8 @@ public function testGetSharesInFolder() { $this->defaults, $this->l10nFactory, $this->urlGenerator, - $this->timeFactory + $this->timeFactory, + $this->cacheAccess, ); $password = md5(time()); @@ -2624,7 +2652,8 @@ public function testGetAccessListNoCurrentAccessRequired() { $this->defaults, $this->l10nFactory, $this->urlGenerator, - $this->timeFactory + $this->timeFactory, + $this->cacheAccess, ); $u1 = $userManager->createUser('testShare1', 'test'); @@ -2720,7 +2749,8 @@ public function testGetAccessListCurrentAccessRequired() { $this->defaults, $this->l10nFactory, $this->urlGenerator, - $this->timeFactory + $this->timeFactory, + $this->cacheAccess, ); $u1 = $userManager->createUser('testShare1', 'test');