UserMountCache.php 13 KB

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