Passed
Push — master ( e0d767...d0a7f8 )
by Roeland
24:12 queued 10:57
created

UserMountCache::updateCachedMount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 1
dl 0
loc 11
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 Lukas Reschke <[email protected]>
8
 * @author Morris Jobke <[email protected]>
9
 * @author Robin Appelman <[email protected]>
10
 * @author Roeland Jago Douma <[email protected]>
11
 * @author Vincent Petry <[email protected]>
12
 *
13
 * @license AGPL-3.0
14
 *
15
 * This code is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License, version 3,
17
 * as published by the Free Software Foundation.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License, version 3,
25
 * along with this program. If not, see <http://www.gnu.org/licenses/>
26
 *
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
				) {
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
			$rows = $query->execute()->fetchAll();
238
239
			$this->mountsForUsers[$user->getUID()] = array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
240
		}
241
		return $this->mountsForUsers[$user->getUID()];
242
	}
243
244
	/**
245
	 * @param int $numericStorageId
246
	 * @param string|null $user limit the results to a single user
247
	 * @return CachedMountInfo[]
248
	 */
249
	public function getMountsForStorageId($numericStorageId, $user = null) {
250
		$builder = $this->connection->getQueryBuilder();
251
		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
252
			->from('mounts', 'm')
253
			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
254
			->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
255
256
		if ($user) {
257
			$query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
258
		}
259
260
		$rows = $query->execute()->fetchAll();
261
262
		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
263
	}
264
265
	/**
266
	 * @param int $rootFileId
267
	 * @return CachedMountInfo[]
268
	 */
269
	public function getMountsForRootId($rootFileId) {
270
		$builder = $this->connection->getQueryBuilder();
271
		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
272
			->from('mounts', 'm')
273
			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
274
			->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT)));
275
276
		$rows = $query->execute()->fetchAll();
277
278
		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
279
	}
280
281
	/**
282
	 * @param $fileId
283
	 * @return array
284
	 * @throws \OCP\Files\NotFoundException
285
	 */
286
	private function getCacheInfoFromFileId($fileId) {
287
		if (!isset($this->cacheInfoCache[$fileId])) {
288
			$builder = $this->connection->getQueryBuilder();
289
			$query = $builder->select('storage', 'path', 'mimetype')
290
				->from('filecache')
291
				->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
292
293
			$row = $query->execute()->fetch();
294
			if (is_array($row)) {
295
				$this->cacheInfoCache[$fileId] = [
296
					(int)$row['storage'],
297
					$row['path'],
298
					(int)$row['mimetype']
299
				];
300
			} else {
301
				throw new NotFoundException('File with id "' . $fileId . '" not found');
302
			}
303
		}
304
		return $this->cacheInfoCache[$fileId];
305
	}
306
307
	/**
308
	 * @param int $fileId
309
	 * @param string|null $user optionally restrict the results to a single user
310
	 * @return ICachedMountFileInfo[]
311
	 * @since 9.0.0
312
	 */
313
	public function getMountsForFileId($fileId, $user = null) {
314
		try {
315
			list($storageId, $internalPath) = $this->getCacheInfoFromFileId($fileId);
316
		} catch (NotFoundException $e) {
317
			return [];
318
		}
319
		$mountsForStorage = $this->getMountsForStorageId($storageId, $user);
320
321
		// filter mounts that are from the same storage but a different directory
322
		$filteredMounts = array_filter($mountsForStorage, function (ICachedMountInfo $mount) use ($internalPath, $fileId) {
323
			if ($fileId === $mount->getRootId()) {
324
				return true;
325
			}
326
			$internalMountPath = $mount->getRootInternalPath();
327
328
			return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/';
329
		});
330
331
		return array_map(function (ICachedMountInfo $mount) use ($internalPath) {
332
			return new CachedMountFileInfo(
333
				$mount->getUser(),
334
				$mount->getStorageId(),
335
				$mount->getRootId(),
336
				$mount->getMountPoint(),
337
				$mount->getMountId(),
338
				$mount->getRootInternalPath(),
339
				$internalPath
340
			);
341
		}, $filteredMounts);
342
	}
343
344
	/**
345
	 * Remove all cached mounts for a user
346
	 *
347
	 * @param IUser $user
348
	 */
349
	public function removeUserMounts(IUser $user) {
350
		$builder = $this->connection->getQueryBuilder();
351
352
		$query = $builder->delete('mounts')
353
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
354
		$query->execute();
355
	}
356
357
	public function removeUserStorageMount($storageId, $userId) {
358
		$builder = $this->connection->getQueryBuilder();
359
360
		$query = $builder->delete('mounts')
361
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
362
			->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
363
		$query->execute();
364
	}
365
366
	public function remoteStorageMounts($storageId) {
367
		$builder = $this->connection->getQueryBuilder();
368
369
		$query = $builder->delete('mounts')
370
			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
371
		$query->execute();
372
	}
373
374
	/**
375
	 * @param array $users
376
	 * @return array
377
	 * @suppress SqlInjectionChecker
378
	 */
379
	public function getUsedSpaceForUsers(array $users) {
380
		$builder = $this->connection->getQueryBuilder();
381
382
		$slash = $builder->createNamedParameter('/');
383
384
		$mountPoint = $builder->func()->concat(
385
			$builder->func()->concat($slash, 'user_id'),
386
			$slash
387
		);
388
389
		$userIds = array_map(function (IUser $user) {
390
			return $user->getUID();
391
		}, $users);
392
393
		$query = $builder->select('m.user_id', 'f.size')
394
			->from('mounts', 'm')
395
			->innerJoin('m', 'filecache', 'f',
396
				$builder->expr()->andX(
0 ignored issues
show
Bug introduced by
$builder->expr()->andX($...rameter(md5('files')))) of type OCP\DB\QueryBuilder\ICompositeExpression is incompatible with the type string expected by parameter $condition of OCP\DB\QueryBuilder\IQueryBuilder::innerJoin(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

396
				/** @scrutinizer ignore-type */ $builder->expr()->andX(
Loading history...
397
					$builder->expr()->eq('m.storage_id', 'f.storage'),
398
					$builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files')))
399
				))
400
			->where($builder->expr()->eq('m.mount_point', $mountPoint))
401
			->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
402
403
		$result = $query->execute();
404
405
		$results = [];
406
		while ($row = $result->fetch()) {
407
			$results[$row['user_id']] = $row['size'];
408
		}
409
		$result->closeCursor();
410
		return $results;
411
	}
412
413
	public function clear(): void {
414
		$this->cacheInfoCache = new CappedMemoryCache();
415
		$this->mountsForUsers = new CappedMemoryCache();
416
	}
417
}
418