You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

417 lines
13 KiB

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