Completed
Push — master ( f9d8eb...989a8a )
by Lukas
14:49
created

UserMountCache::getMountsForFileId()   B

Complexity

Conditions 4
Paths 2

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 21
nc 2
nop 2
dl 0
loc 30
rs 8.5806
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Joas Schilling <[email protected]>
6
 * @author Lukas Reschke <[email protected]>
7
 * @author Robin Appelman <[email protected]>
8
 * @author Vincent Petry <[email protected]>
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
26
namespace OC\Files\Config;
27
28
use OC\DB\QueryBuilder\Literal;
29
use OCA\Files_Sharing\SharedMount;
30
use OCP\DB\QueryBuilder\IQueryBuilder;
31
use OCP\Files\Config\ICachedMountFileInfo;
32
use OCP\Files\Config\ICachedMountInfo;
33
use OCP\Files\Config\IUserMountCache;
34
use OCP\Files\Mount\IMountPoint;
35
use OCP\Files\NotFoundException;
36
use OCP\ICache;
37
use OCP\IDBConnection;
38
use OCP\ILogger;
39
use OCP\IUser;
40
use OCP\IUserManager;
41
use OC\Cache\CappedMemoryCache;
42
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
	/**
53
	 * @var IUserManager
54
	 */
55
	private $userManager;
56
57
	/**
58
	 * Cached mount info.
59
	 * Map of $userId to ICachedMountInfo.
60
	 *
61
	 * @var ICache
62
	 **/
63
	private $mountsForUsers;
64
65
	/**
66
	 * @var ILogger
67
	 */
68
	private $logger;
69
70
	/**
71
	 * @var ICache
72
	 */
73
	private $cacheInfoCache;
74
75
	/**
76
	 * UserMountCache constructor.
77
	 *
78
	 * @param IDBConnection $connection
79
	 * @param IUserManager $userManager
80
	 * @param ILogger $logger
81
	 */
82
	public function __construct(IDBConnection $connection, IUserManager $userManager, ILogger $logger) {
83
		$this->connection = $connection;
84
		$this->userManager = $userManager;
85
		$this->logger = $logger;
86
		$this->cacheInfoCache = new CappedMemoryCache();
87
		$this->mountsForUsers = new CappedMemoryCache();
88
	}
89
90
	public function registerMounts(IUser $user, array $mounts) {
91
		// filter out non-proper storages coming from unit tests
92
		$mounts = array_filter($mounts, function (IMountPoint $mount) {
93
			return $mount instanceof SharedMount || $mount->getStorage() && $mount->getStorage()->getCache();
94
		});
95
		/** @var ICachedMountInfo[] $newMounts */
96
		$newMounts = array_map(function (IMountPoint $mount) use ($user) {
97
			// filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
98
			if ($mount->getStorageRootId() === -1) {
99
				return null;
100
			} else {
101
				return new LazyStorageMountInfo($user, $mount);
102
			}
103
		}, $mounts);
104
		$newMounts = array_values(array_filter($newMounts));
105
106
		$cachedMounts = $this->getMountsForUser($user);
107
		$mountDiff = function (ICachedMountInfo $mount1, ICachedMountInfo $mount2) {
108
			// since we are only looking for mounts for a specific user comparing on root id is enough
109
			return $mount1->getRootId() - $mount2->getRootId();
110
		};
111
112
		/** @var ICachedMountInfo[] $addedMounts */
113
		$addedMounts = array_udiff($newMounts, $cachedMounts, $mountDiff);
114
		/** @var ICachedMountInfo[] $removedMounts */
115
		$removedMounts = array_udiff($cachedMounts, $newMounts, $mountDiff);
116
117
		$changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
118
119
		foreach ($addedMounts as $mount) {
120
			$this->addToCache($mount);
121
			$this->mountsForUsers[$user->getUID()][] = $mount;
122
		}
123
		foreach ($removedMounts as $mount) {
124
			$this->removeFromCache($mount);
125
			$index = array_search($mount, $this->mountsForUsers[$user->getUID()]);
126
			unset($this->mountsForUsers[$user->getUID()][$index]);
127
		}
128
		foreach ($changedMounts as $mount) {
129
			$this->updateCachedMount($mount);
130
		}
131
	}
132
133
	/**
134
	 * @param ICachedMountInfo[] $newMounts
135
	 * @param ICachedMountInfo[] $cachedMounts
136
	 * @return ICachedMountInfo[]
137
	 */
138
	private function findChangedMounts(array $newMounts, array $cachedMounts) {
139
		$changed = [];
140
		foreach ($newMounts as $newMount) {
141
			foreach ($cachedMounts as $cachedMount) {
142
				if (
143
					$newMount->getRootId() === $cachedMount->getRootId() &&
144
					(
145
						$newMount->getMountPoint() !== $cachedMount->getMountPoint() ||
146
						$newMount->getStorageId() !== $cachedMount->getStorageId() ||
147
						$newMount->getMountId() !== $cachedMount->getMountId()
148
					)
149
				) {
150
					$changed[] = $newMount;
151
				}
152
			}
153
		}
154
		return $changed;
155
	}
156
157
	private function addToCache(ICachedMountInfo $mount) {
158
		if ($mount->getStorageId() !== -1) {
159
			$this->connection->insertIfNotExist('*PREFIX*mounts', [
160
				'storage_id' => $mount->getStorageId(),
161
				'root_id' => $mount->getRootId(),
162
				'user_id' => $mount->getUser()->getUID(),
163
				'mount_point' => $mount->getMountPoint(),
164
				'mount_id' => $mount->getMountId()
165
			], ['root_id', 'user_id']);
166
		} else {
167
			// in some cases this is legitimate, like orphaned shares
168
			$this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
169
		}
170
	}
171
172
	private function updateCachedMount(ICachedMountInfo $mount) {
173
		$builder = $this->connection->getQueryBuilder();
174
175
		$query = $builder->update('mounts')
176
			->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
177
			->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
178
			->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
179
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
180
			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
181
182
		$query->execute();
183
	}
184
185 View Code Duplication
	private function removeFromCache(ICachedMountInfo $mount) {
186
		$builder = $this->connection->getQueryBuilder();
187
188
		$query = $builder->delete('mounts')
189
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
190
			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
191
		$query->execute();
192
	}
193
194
	private function dbRowToMountInfo(array $row) {
195
		$user = $this->userManager->get($row['user_id']);
196
		if (is_null($user)) {
197
			return null;
198
		}
199
		$mount_id = $row['mount_id'];
200
		if (!is_null($mount_id)) {
201
			$mount_id = (int) $mount_id;
202
		}
203
		return new CachedMountInfo($user, (int)$row['storage_id'], (int)$row['root_id'], $row['mount_point'], $mount_id, isset($row['path']) ? $row['path'] : '');
204
	}
205
206
	/**
207
	 * @param IUser $user
208
	 * @return ICachedMountInfo[]
209
	 */
210
	public function getMountsForUser(IUser $user) {
211
		if (!isset($this->mountsForUsers[$user->getUID()])) {
212
			$builder = $this->connection->getQueryBuilder();
213
			$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
214
				->from('mounts', 'm')
215
				->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
216
				->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($user->getUID())));
217
218
			$rows = $query->execute()->fetchAll();
219
220
			$this->mountsForUsers[$user->getUID()] = array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
221
		}
222
		return $this->mountsForUsers[$user->getUID()];
223
	}
224
225
	/**
226
	 * @param int $numericStorageId
227
	 * @param string|null $user limit the results to a single user
228
	 * @return CachedMountInfo[]
229
	 */
230
	public function getMountsForStorageId($numericStorageId, $user = null) {
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('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
236
237
		if ($user) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $user of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
238
			$query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
239
		}
240
241
		$rows = $query->execute()->fetchAll();
242
243
		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
244
	}
245
246
	/**
247
	 * @param int $rootFileId
248
	 * @return CachedMountInfo[]
249
	 */
250
	public function getMountsForRootId($rootFileId) {
251
		$builder = $this->connection->getQueryBuilder();
252
		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
253
			->from('mounts', 'm')
254
			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
255
			->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT)));
256
257
		$rows = $query->execute()->fetchAll();
258
259
		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
260
	}
261
262
	/**
263
	 * @param $fileId
264
	 * @return array
265
	 * @throws \OCP\Files\NotFoundException
266
	 */
267
	private function getCacheInfoFromFileId($fileId) {
268
		if (!isset($this->cacheInfoCache[$fileId])) {
269
			$builder = $this->connection->getQueryBuilder();
270
			$query = $builder->select('storage', 'path', 'mimetype')
271
				->from('filecache')
272
				->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
273
274
			$row = $query->execute()->fetch();
275
			if (is_array($row)) {
276
				$this->cacheInfoCache[$fileId] = [
277
					(int)$row['storage'],
278
					$row['path'],
279
					(int)$row['mimetype']
280
				];
281
			} else {
282
				throw new NotFoundException('File with id "' . $fileId . '" not found');
283
			}
284
		}
285
		return $this->cacheInfoCache[$fileId];
286
	}
287
288
	/**
289
	 * @param int $fileId
290
	 * @param string|null $user optionally restrict the results to a single user
291
	 * @return ICachedMountFileInfo[]
292
	 * @since 9.0.0
293
	 */
294
	public function getMountsForFileId($fileId, $user = null) {
295
		try {
296
			list($storageId, $internalPath) = $this->getCacheInfoFromFileId($fileId);
297
		} catch (NotFoundException $e) {
298
			return [];
299
		}
300
		$mountsForStorage = $this->getMountsForStorageId($storageId, $user);
301
302
		// filter mounts that are from the same storage but a different directory
303
		$filteredMounts = array_filter($mountsForStorage, function (ICachedMountInfo $mount) use ($internalPath, $fileId) {
304
			if ($fileId === $mount->getRootId()) {
305
				return true;
306
			}
307
			$internalMountPath = $mount->getRootInternalPath();
308
309
			return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/';
310
		});
311
312
		return array_map(function (ICachedMountInfo $mount) use ($internalPath) {
313
			return new CachedMountFileInfo(
314
				$mount->getUser(),
315
				$mount->getStorageId(),
316
				$mount->getRootId(),
317
				$mount->getMountPoint(),
318
				$mount->getMountId(),
319
				$mount->getRootInternalPath(),
320
				$internalPath
321
			);
322
		}, $filteredMounts);
323
	}
324
325
	/**
326
	 * Remove all cached mounts for a user
327
	 *
328
	 * @param IUser $user
329
	 */
330 View Code Duplication
	public function removeUserMounts(IUser $user) {
331
		$builder = $this->connection->getQueryBuilder();
332
333
		$query = $builder->delete('mounts')
334
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
335
		$query->execute();
336
	}
337
338 View Code Duplication
	public function removeUserStorageMount($storageId, $userId) {
339
		$builder = $this->connection->getQueryBuilder();
340
341
		$query = $builder->delete('mounts')
342
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
343
			->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
344
		$query->execute();
345
	}
346
347 View Code Duplication
	public function remoteStorageMounts($storageId) {
348
		$builder = $this->connection->getQueryBuilder();
349
350
		$query = $builder->delete('mounts')
351
			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
352
		$query->execute();
353
	}
354
355
	/**
356
	 * @param array $users
357
	 * @return array
358
	 * @suppress SqlInjectionChecker
359
	 */
360
	public function getUsedSpaceForUsers(array $users) {
361
		$builder = $this->connection->getQueryBuilder();
362
363
		$slash = $builder->createNamedParameter('/');
364
365
		$mountPoint = $builder->func()->concat(
366
			$builder->func()->concat($slash, 'user_id'),
367
			$slash
368
		);
369
370
		$userIds = array_map(function (IUser $user) {
371
			return $user->getUID();
372
		}, $users);
373
374
		$query = $builder->select('m.user_id', 'f.size')
375
			->from('mounts', 'm')
376
			->innerJoin('m', 'filecache', 'f',
377
				$builder->expr()->andX(
0 ignored issues
show
Documentation introduced by
$builder->expr()->andX($...medParameter('files'))) is of type object<OCP\DB\QueryBuilder\ICompositeExpression>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
378
					$builder->expr()->eq('m.storage_id', 'f.storage'),
379
					$builder->expr()->eq('f.path', $builder->createNamedParameter('files'))
380
				))
381
			->where($builder->expr()->eq('m.mount_point', $mountPoint))
382
			->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
383
384
		$result = $query->execute();
385
386
		$results = [];
387
		while ($row = $result->fetch()) {
388
			$results[$row['user_id']] = $row['size'];
389
		}
390
		$result->closeCursor();
391
		return $results;
392
	}
393
}
394