Passed
Push — master ( 692da9...451f70 )
by Robin
13:23 queued 15s
created

UserMountCache::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 3
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Dariusz Olszewski <[email protected]>
6
 * @author Joas Schilling <[email protected]>
7
 * @author Julius Härtl <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Vincent Petry <[email protected]>
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
31
use OC\Cache\CappedMemoryCache;
32
use OCA\Files_Sharing\SharedMount;
33
use OCP\DB\QueryBuilder\IQueryBuilder;
34
use OCP\Files\Config\ICachedMountFileInfo;
35
use OCP\Files\Config\ICachedMountInfo;
36
use OCP\Files\Config\IUserMountCache;
37
use OCP\Files\Mount\IMountPoint;
38
use OCP\Files\NotFoundException;
39
use OCP\ICache;
40
use OCP\IDBConnection;
41
use OCP\ILogger;
42
use OCP\IUser;
43
use OCP\IUserManager;
44
45
/**
46
 * Cache mounts points per user in the cache so we can easilly look them up
47
 */
48
class 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
					$newMount->getMountProvider() !== $cachedMount->getMountProvider()
169
				) {
170
					$changed[] = $newMount;
171
				}
172
			}
173
		}
174
		return $changed;
175
	}
176
177
	private function addToCache(ICachedMountInfo $mount) {
178
		if ($mount->getStorageId() !== -1) {
179
			$this->connection->insertIfNotExist('*PREFIX*mounts', [
180
				'storage_id' => $mount->getStorageId(),
181
				'root_id' => $mount->getRootId(),
182
				'user_id' => $mount->getUser()->getUID(),
183
				'mount_point' => $mount->getMountPoint(),
184
				'mount_id' => $mount->getMountId(),
185
				'mount_provider_class' => $mount->getMountProvider(),
186
			], ['root_id', 'user_id']);
187
		} else {
188
			// in some cases this is legitimate, like orphaned shares
189
			$this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
190
		}
191
	}
192
193
	private function updateCachedMount(ICachedMountInfo $mount) {
194
		$builder = $this->connection->getQueryBuilder();
195
196
		$query = $builder->update('mounts')
197
			->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
198
			->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
199
			->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
200
			->set('mount_provider_class', $builder->createNamedParameter($mount->getMountProvider()))
201
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
202
			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
203
204
		$query->execute();
205
	}
206
207
	private function removeFromCache(ICachedMountInfo $mount) {
208
		$builder = $this->connection->getQueryBuilder();
209
210
		$query = $builder->delete('mounts')
211
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
212
			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
213
		$query->execute();
214
	}
215
216
	private function dbRowToMountInfo(array $row) {
217
		$user = $this->userManager->get($row['user_id']);
218
		if (is_null($user)) {
219
			return null;
220
		}
221
		$mount_id = $row['mount_id'];
222
		if (!is_null($mount_id)) {
223
			$mount_id = (int)$mount_id;
224
		}
225
		return new CachedMountInfo(
226
			$user,
227
			(int)$row['storage_id'],
228
			(int)$row['root_id'],
229
			$row['mount_point'],
230
			$row['mount_provider_class'] ?? '',
231
			$mount_id,
232
			isset($row['path']) ? $row['path'] : '',
233
		);
234
	}
235
236
	/**
237
	 * @param IUser $user
238
	 * @return ICachedMountInfo[]
239
	 */
240
	public function getMountsForUser(IUser $user) {
241
		if (!isset($this->mountsForUsers[$user->getUID()])) {
242
			$builder = $this->connection->getQueryBuilder();
243
			$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
244
				->from('mounts', 'm')
245
				->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
246
				->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($user->getUID())));
247
248
			$result = $query->execute();
249
			$rows = $result->fetchAll();
250
			$result->closeCursor();
251
252
			$this->mountsForUsers[$user->getUID()] = array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
253
		}
254
		return $this->mountsForUsers[$user->getUID()];
255
	}
256
257
	/**
258
	 * @param int $numericStorageId
259
	 * @param string|null $user limit the results to a single user
260
	 * @return CachedMountInfo[]
261
	 */
262
	public function getMountsForStorageId($numericStorageId, $user = null) {
263
		$builder = $this->connection->getQueryBuilder();
264
		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
265
			->from('mounts', 'm')
266
			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
267
			->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
268
269
		if ($user) {
270
			$query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
271
		}
272
273
		$result = $query->execute();
274
		$rows = $result->fetchAll();
275
		$result->closeCursor();
276
277
		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
278
	}
279
280
	/**
281
	 * @param int $rootFileId
282
	 * @return CachedMountInfo[]
283
	 */
284
	public function getMountsForRootId($rootFileId) {
285
		$builder = $this->connection->getQueryBuilder();
286
		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
287
			->from('mounts', 'm')
288
			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
289
			->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT)));
290
291
		$result = $query->execute();
292
		$rows = $result->fetchAll();
293
		$result->closeCursor();
294
295
		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
296
	}
297
298
	/**
299
	 * @param $fileId
300
	 * @return array{int, string, int}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{int, string, int} at position 2 could not be parsed: Expected ':' at position 2, but found 'int'.
Loading history...
301
	 * @throws \OCP\Files\NotFoundException
302
	 */
303
	private function getCacheInfoFromFileId($fileId): array {
304
		if (!isset($this->cacheInfoCache[$fileId])) {
305
			$builder = $this->connection->getQueryBuilder();
306
			$query = $builder->select('storage', 'path', 'mimetype')
307
				->from('filecache')
308
				->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
309
310
			$result = $query->execute();
311
			$row = $result->fetch();
312
			$result->closeCursor();
313
314
			if (is_array($row)) {
315
				$this->cacheInfoCache[$fileId] = [
316
					(int)$row['storage'],
317
					(string)$row['path'],
318
					(int)$row['mimetype']
319
				];
320
			} else {
321
				throw new NotFoundException('File with id "' . $fileId . '" not found');
322
			}
323
		}
324
		return $this->cacheInfoCache[$fileId];
325
	}
326
327
	/**
328
	 * @param int $fileId
329
	 * @param string|null $user optionally restrict the results to a single user
330
	 * @return ICachedMountFileInfo[]
331
	 * @since 9.0.0
332
	 */
333
	public function getMountsForFileId($fileId, $user = null) {
334
		try {
335
			[$storageId, $internalPath] = $this->getCacheInfoFromFileId($fileId);
336
		} catch (NotFoundException $e) {
337
			return [];
338
		}
339
		$builder = $this->connection->getQueryBuilder();
340
		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
341
			->from('mounts', 'm')
342
			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
343
			->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($storageId, IQueryBuilder::PARAM_INT)));
344
345
		if ($user) {
346
			$query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
347
		}
348
349
		$result = $query->execute();
350
		$rows = $result->fetchAll();
351
		$result->closeCursor();
352
		// filter mounts that are from the same storage but a different directory
353
		$filteredMounts = array_filter($rows, function (array $row) use ($internalPath, $fileId) {
354
			if ($fileId === (int)$row['root_id']) {
355
				return true;
356
			}
357
			$internalMountPath = $row['path'] ?? '';
358
359
			return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/';
360
		});
361
362
		$filteredMounts = array_filter(array_map([$this, 'dbRowToMountInfo'], $filteredMounts));
363
		return array_map(function (ICachedMountInfo $mount) use ($internalPath) {
364
			return new CachedMountFileInfo(
365
				$mount->getUser(),
366
				$mount->getStorageId(),
367
				$mount->getRootId(),
368
				$mount->getMountPoint(),
369
				$mount->getMountId(),
370
				$mount->getMountProvider(),
371
				$mount->getRootInternalPath(),
372
				$internalPath
373
			);
374
		}, $filteredMounts);
375
	}
376
377
	/**
378
	 * Remove all cached mounts for a user
379
	 *
380
	 * @param IUser $user
381
	 */
382
	public function removeUserMounts(IUser $user) {
383
		$builder = $this->connection->getQueryBuilder();
384
385
		$query = $builder->delete('mounts')
386
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
387
		$query->execute();
388
	}
389
390
	public function removeUserStorageMount($storageId, $userId) {
391
		$builder = $this->connection->getQueryBuilder();
392
393
		$query = $builder->delete('mounts')
394
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
395
			->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
396
		$query->execute();
397
	}
398
399
	public function remoteStorageMounts($storageId) {
400
		$builder = $this->connection->getQueryBuilder();
401
402
		$query = $builder->delete('mounts')
403
			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
404
		$query->execute();
405
	}
406
407
	/**
408
	 * @param array $users
409
	 * @return array
410
	 */
411
	public function getUsedSpaceForUsers(array $users) {
412
		$builder = $this->connection->getQueryBuilder();
413
414
		$slash = $builder->createNamedParameter('/');
415
416
		$mountPoint = $builder->func()->concat(
417
			$builder->func()->concat($slash, 'user_id'),
418
			$slash
419
		);
420
421
		$userIds = array_map(function (IUser $user) {
422
			return $user->getUID();
423
		}, $users);
424
425
		$query = $builder->select('m.user_id', 'f.size')
426
			->from('mounts', 'm')
427
			->innerJoin('m', 'filecache', 'f',
428
				$builder->expr()->andX(
429
					$builder->expr()->eq('m.storage_id', 'f.storage'),
430
					$builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files')))
431
				))
432
			->where($builder->expr()->eq('m.mount_point', $mountPoint))
433
			->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
434
435
		$result = $query->execute();
436
437
		$results = [];
438
		while ($row = $result->fetch()) {
439
			$results[$row['user_id']] = $row['size'];
440
		}
441
		$result->closeCursor();
442
		return $results;
443
	}
444
445
	public function clear(): void {
446
		$this->cacheInfoCache = new CappedMemoryCache();
447
		$this->mountsForUsers = new CappedMemoryCache();
448
	}
449
}
450