1<?php 2/** 3 * @copyright Copyright (c) 2016, ownCloud, Inc. 4 * 5 * @author Dariusz Olszewski <starypatyk@users.noreply.github.com> 6 * @author Joas Schilling <coding@schilljs.com> 7 * @author Julius Härtl <jus@bitgrid.net> 8 * @author Lukas Reschke <lukas@statuscode.ch> 9 * @author Morris Jobke <hey@morrisjobke.de> 10 * @author Robin Appelman <robin@icewind.nl> 11 * @author Roeland Jago Douma <roeland@famdouma.nl> 12 * @author Vincent Petry <vincent@nextcloud.com> 13 * 14 * @license AGPL-3.0 15 * 16 * This code is free software: you can redistribute it and/or modify 17 * it under the terms of the GNU Affero General Public License, version 3, 18 * as published by the Free Software Foundation. 19 * 20 * This program is distributed in the hope that it will be useful, 21 * but WITHOUT ANY WARRANTY; without even the implied warranty of 22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 * GNU Affero General Public License for more details. 24 * 25 * You should have received a copy of the GNU Affero General Public License, version 3, 26 * along with this program. If not, see <http://www.gnu.org/licenses/> 27 * 28 */ 29namespace OC\Files\Config; 30 31use OC\Cache\CappedMemoryCache; 32use OCA\Files_Sharing\SharedMount; 33use OCP\DB\QueryBuilder\IQueryBuilder; 34use OCP\Files\Config\ICachedMountFileInfo; 35use OCP\Files\Config\ICachedMountInfo; 36use OCP\Files\Config\IUserMountCache; 37use OCP\Files\Mount\IMountPoint; 38use OCP\Files\NotFoundException; 39use OCP\ICache; 40use OCP\IDBConnection; 41use OCP\ILogger; 42use OCP\IUser; 43use OCP\IUserManager; 44 45/** 46 * Cache mounts points per user in the cache so we can easilly look them up 47 */ 48class UserMountCache implements IUserMountCache { 49 /** 50 * @var IDBConnection 51 */ 52 private $connection; 53 54 /** 55 * @var IUserManager 56 */ 57 private $userManager; 58 59 /** 60 * Cached mount info. 61 * Map of $userId to ICachedMountInfo. 62 * 63 * @var ICache 64 **/ 65 private $mountsForUsers; 66 67 /** 68 * @var ILogger 69 */ 70 private $logger; 71 72 /** 73 * @var ICache 74 */ 75 private $cacheInfoCache; 76 77 /** 78 * UserMountCache constructor. 79 * 80 * @param IDBConnection $connection 81 * @param IUserManager $userManager 82 * @param ILogger $logger 83 */ 84 public function __construct(IDBConnection $connection, IUserManager $userManager, ILogger $logger) { 85 $this->connection = $connection; 86 $this->userManager = $userManager; 87 $this->logger = $logger; 88 $this->cacheInfoCache = new CappedMemoryCache(); 89 $this->mountsForUsers = new CappedMemoryCache(); 90 } 91 92 public function registerMounts(IUser $user, array $mounts) { 93 // filter out non-proper storages coming from unit tests 94 $mounts = array_filter($mounts, function (IMountPoint $mount) { 95 return $mount instanceof SharedMount || $mount->getStorage() && $mount->getStorage()->getCache(); 96 }); 97 /** @var ICachedMountInfo[] $newMounts */ 98 $newMounts = array_map(function (IMountPoint $mount) use ($user) { 99 // filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet) 100 if ($mount->getStorageRootId() === -1) { 101 return null; 102 } else { 103 return new LazyStorageMountInfo($user, $mount); 104 } 105 }, $mounts); 106 $newMounts = array_values(array_filter($newMounts)); 107 $newMountRootIds = array_map(function (ICachedMountInfo $mount) { 108 return $mount->getRootId(); 109 }, $newMounts); 110 $newMounts = array_combine($newMountRootIds, $newMounts); 111 112 $cachedMounts = $this->getMountsForUser($user); 113 $cachedMountRootIds = array_map(function (ICachedMountInfo $mount) { 114 return $mount->getRootId(); 115 }, $cachedMounts); 116 $cachedMounts = array_combine($cachedMountRootIds, $cachedMounts); 117 118 $addedMounts = []; 119 $removedMounts = []; 120 121 foreach ($newMounts as $rootId => $newMount) { 122 if (!isset($cachedMounts[$rootId])) { 123 $addedMounts[] = $newMount; 124 } 125 } 126 127 foreach ($cachedMounts as $rootId => $cachedMount) { 128 if (!isset($newMounts[$rootId])) { 129 $removedMounts[] = $cachedMount; 130 } 131 } 132 133 $changedMounts = $this->findChangedMounts($newMounts, $cachedMounts); 134 135 foreach ($addedMounts as $mount) { 136 $this->addToCache($mount); 137 $this->mountsForUsers[$user->getUID()][] = $mount; 138 } 139 foreach ($removedMounts as $mount) { 140 $this->removeFromCache($mount); 141 $index = array_search($mount, $this->mountsForUsers[$user->getUID()]); 142 unset($this->mountsForUsers[$user->getUID()][$index]); 143 } 144 foreach ($changedMounts as $mount) { 145 $this->updateCachedMount($mount); 146 } 147 } 148 149 /** 150 * @param ICachedMountInfo[] $newMounts 151 * @param ICachedMountInfo[] $cachedMounts 152 * @return ICachedMountInfo[] 153 */ 154 private function findChangedMounts(array $newMounts, array $cachedMounts) { 155 $new = []; 156 foreach ($newMounts as $mount) { 157 $new[$mount->getRootId()] = $mount; 158 } 159 $changed = []; 160 foreach ($cachedMounts as $cachedMount) { 161 $rootId = $cachedMount->getRootId(); 162 if (isset($new[$rootId])) { 163 $newMount = $new[$rootId]; 164 if ( 165 $newMount->getMountPoint() !== $cachedMount->getMountPoint() || 166 $newMount->getStorageId() !== $cachedMount->getStorageId() || 167 $newMount->getMountId() !== $cachedMount->getMountId() 168 ) { 169 $changed[] = $newMount; 170 } 171 } 172 } 173 return $changed; 174 } 175 176 private function addToCache(ICachedMountInfo $mount) { 177 if ($mount->getStorageId() !== -1) { 178 $this->connection->insertIfNotExist('*PREFIX*mounts', [ 179 'storage_id' => $mount->getStorageId(), 180 'root_id' => $mount->getRootId(), 181 'user_id' => $mount->getUser()->getUID(), 182 'mount_point' => $mount->getMountPoint(), 183 'mount_id' => $mount->getMountId() 184 ], ['root_id', 'user_id']); 185 } else { 186 // in some cases this is legitimate, like orphaned shares 187 $this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint()); 188 } 189 } 190 191 private function updateCachedMount(ICachedMountInfo $mount) { 192 $builder = $this->connection->getQueryBuilder(); 193 194 $query = $builder->update('mounts') 195 ->set('storage_id', $builder->createNamedParameter($mount->getStorageId())) 196 ->set('mount_point', $builder->createNamedParameter($mount->getMountPoint())) 197 ->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT)) 198 ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID()))) 199 ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT))); 200 201 $query->execute(); 202 } 203 204 private function removeFromCache(ICachedMountInfo $mount) { 205 $builder = $this->connection->getQueryBuilder(); 206 207 $query = $builder->delete('mounts') 208 ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID()))) 209 ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT))); 210 $query->execute(); 211 } 212 213 private function dbRowToMountInfo(array $row) { 214 $user = $this->userManager->get($row['user_id']); 215 if (is_null($user)) { 216 return null; 217 } 218 $mount_id = $row['mount_id']; 219 if (!is_null($mount_id)) { 220 $mount_id = (int)$mount_id; 221 } 222 return new CachedMountInfo($user, (int)$row['storage_id'], (int)$row['root_id'], $row['mount_point'], $mount_id, isset($row['path']) ? $row['path'] : ''); 223 } 224 225 /** 226 * @param IUser $user 227 * @return ICachedMountInfo[] 228 */ 229 public function getMountsForUser(IUser $user) { 230 if (!isset($this->mountsForUsers[$user->getUID()])) { 231 $builder = $this->connection->getQueryBuilder(); 232 $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path') 233 ->from('mounts', 'm') 234 ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) 235 ->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($user->getUID()))); 236 237 $result = $query->execute(); 238 $rows = $result->fetchAll(); 239 $result->closeCursor(); 240 241 $this->mountsForUsers[$user->getUID()] = array_filter(array_map([$this, 'dbRowToMountInfo'], $rows)); 242 } 243 return $this->mountsForUsers[$user->getUID()]; 244 } 245 246 /** 247 * @param int $numericStorageId 248 * @param string|null $user limit the results to a single user 249 * @return CachedMountInfo[] 250 */ 251 public function getMountsForStorageId($numericStorageId, $user = null) { 252 $builder = $this->connection->getQueryBuilder(); 253 $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path') 254 ->from('mounts', 'm') 255 ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) 256 ->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT))); 257 258 if ($user) { 259 $query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user))); 260 } 261 262 $result = $query->execute(); 263 $rows = $result->fetchAll(); 264 $result->closeCursor(); 265 266 return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows)); 267 } 268 269 /** 270 * @param int $rootFileId 271 * @return CachedMountInfo[] 272 */ 273 public function getMountsForRootId($rootFileId) { 274 $builder = $this->connection->getQueryBuilder(); 275 $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path') 276 ->from('mounts', 'm') 277 ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) 278 ->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT))); 279 280 $result = $query->execute(); 281 $rows = $result->fetchAll(); 282 $result->closeCursor(); 283 284 return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows)); 285 } 286 287 /** 288 * @param $fileId 289 * @return array 290 * @throws \OCP\Files\NotFoundException 291 */ 292 private function getCacheInfoFromFileId($fileId) { 293 if (!isset($this->cacheInfoCache[$fileId])) { 294 $builder = $this->connection->getQueryBuilder(); 295 $query = $builder->select('storage', 'path', 'mimetype') 296 ->from('filecache') 297 ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); 298 299 $result = $query->execute(); 300 $row = $result->fetch(); 301 $result->closeCursor(); 302 303 if (is_array($row)) { 304 $this->cacheInfoCache[$fileId] = [ 305 (int)$row['storage'], 306 $row['path'], 307 (int)$row['mimetype'] 308 ]; 309 } else { 310 throw new NotFoundException('File with id "' . $fileId . '" not found'); 311 } 312 } 313 return $this->cacheInfoCache[$fileId]; 314 } 315 316 /** 317 * @param int $fileId 318 * @param string|null $user optionally restrict the results to a single user 319 * @return ICachedMountFileInfo[] 320 * @since 9.0.0 321 */ 322 public function getMountsForFileId($fileId, $user = null) { 323 try { 324 [$storageId, $internalPath] = $this->getCacheInfoFromFileId($fileId); 325 } catch (NotFoundException $e) { 326 return []; 327 } 328 $builder = $this->connection->getQueryBuilder(); 329 $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path') 330 ->from('mounts', 'm') 331 ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid')) 332 ->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($storageId, IQueryBuilder::PARAM_INT))); 333 334 if ($user) { 335 $query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user))); 336 } 337 338 $result = $query->execute(); 339 $rows = $result->fetchAll(); 340 $result->closeCursor(); 341 // filter mounts that are from the same storage but a different directory 342 $filteredMounts = array_filter($rows, function (array $row) use ($internalPath, $fileId) { 343 if ($fileId === (int)$row['root_id']) { 344 return true; 345 } 346 $internalMountPath = isset($row['path']) ? $row['path'] : ''; 347 348 return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/'; 349 }); 350 351 $filteredMounts = array_filter(array_map([$this, 'dbRowToMountInfo'], $filteredMounts)); 352 return array_map(function (ICachedMountInfo $mount) use ($internalPath) { 353 return new CachedMountFileInfo( 354 $mount->getUser(), 355 $mount->getStorageId(), 356 $mount->getRootId(), 357 $mount->getMountPoint(), 358 $mount->getMountId(), 359 $mount->getRootInternalPath(), 360 $internalPath 361 ); 362 }, $filteredMounts); 363 } 364 365 /** 366 * Remove all cached mounts for a user 367 * 368 * @param IUser $user 369 */ 370 public function removeUserMounts(IUser $user) { 371 $builder = $this->connection->getQueryBuilder(); 372 373 $query = $builder->delete('mounts') 374 ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID()))); 375 $query->execute(); 376 } 377 378 public function removeUserStorageMount($storageId, $userId) { 379 $builder = $this->connection->getQueryBuilder(); 380 381 $query = $builder->delete('mounts') 382 ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId))) 383 ->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))); 384 $query->execute(); 385 } 386 387 public function remoteStorageMounts($storageId) { 388 $builder = $this->connection->getQueryBuilder(); 389 390 $query = $builder->delete('mounts') 391 ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))); 392 $query->execute(); 393 } 394 395 /** 396 * @param array $users 397 * @return array 398 */ 399 public function getUsedSpaceForUsers(array $users) { 400 $builder = $this->connection->getQueryBuilder(); 401 402 $slash = $builder->createNamedParameter('/'); 403 404 $mountPoint = $builder->func()->concat( 405 $builder->func()->concat($slash, 'user_id'), 406 $slash 407 ); 408 409 $userIds = array_map(function (IUser $user) { 410 return $user->getUID(); 411 }, $users); 412 413 $query = $builder->select('m.user_id', 'f.size') 414 ->from('mounts', 'm') 415 ->innerJoin('m', 'filecache', 'f', 416 $builder->expr()->andX( 417 $builder->expr()->eq('m.storage_id', 'f.storage'), 418 $builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files'))) 419 )) 420 ->where($builder->expr()->eq('m.mount_point', $mountPoint)) 421 ->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY))); 422 423 $result = $query->execute(); 424 425 $results = []; 426 while ($row = $result->fetch()) { 427 $results[$row['user_id']] = $row['size']; 428 } 429 $result->closeCursor(); 430 return $results; 431 } 432 433 public function clear(): void { 434 $this->cacheInfoCache = new CappedMemoryCache(); 435 $this->mountsForUsers = new CappedMemoryCache(); 436 } 437} 438