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