Completed
Pull Request — master (#5342)
by Morris
16:23
created

UserMountCache::getMountsForFileId()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 2
nop 2
dl 0
loc 18
rs 9.2
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 Robin Appelman <[email protected]>
7
 * @author Vincent Petry <[email protected]>
8
 *
9
 * @license AGPL-3.0
10
 *
11
 * This code is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License, version 3,
13
 * as published by the Free Software Foundation.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License, version 3,
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
22
 *
23
 */
24
25
namespace OC\Files\Config;
26
27
use OC\DB\QueryBuilder\Literal;
28
use OCA\Files_Sharing\SharedMount;
29
use OCP\DB\QueryBuilder\IQueryBuilder;
30
use OCP\Files\Config\ICachedMountInfo;
31
use OCP\Files\Config\IUserMountCache;
32
use OCP\Files\Mount\IMountPoint;
33
use OCP\Files\NotFoundException;
34
use OCP\ICache;
35
use OCP\IDBConnection;
36
use OCP\ILogger;
37
use OCP\IUser;
38
use OCP\IUserManager;
39
use OC\Cache\CappedMemoryCache;
40
41
/**
42
 * Cache mounts points per user in the cache so we can easilly look them up
43
 */
44
class UserMountCache implements IUserMountCache {
45
	/**
46
	 * @var IDBConnection
47
	 */
48
	private $connection;
49
50
	/**
51
	 * @var IUserManager
52
	 */
53
	private $userManager;
54
55
	/**
56
	 * Cached mount info.
57
	 * Map of $userId to ICachedMountInfo.
58
	 *
59
	 * @var ICache
60
	 **/
61
	private $mountsForUsers;
62
63
	/**
64
	 * @var ILogger
65
	 */
66
	private $logger;
67
68
	/**
69
	 * @var ICache
70
	 */
71
	private $cacheInfoCache;
72
73
	/**
74
	 * UserMountCache constructor.
75
	 *
76
	 * @param IDBConnection $connection
77
	 * @param IUserManager $userManager
78
	 * @param ILogger $logger
79
	 */
80
	public function __construct(IDBConnection $connection, IUserManager $userManager, ILogger $logger) {
81
		$this->connection = $connection;
82
		$this->userManager = $userManager;
83
		$this->logger = $logger;
84
		$this->cacheInfoCache = new CappedMemoryCache();
85
		$this->mountsForUsers = new CappedMemoryCache();
86
	}
87
88
	public function registerMounts(IUser $user, array $mounts) {
89
		// filter out non-proper storages coming from unit tests
90
		$mounts = array_filter($mounts, function (IMountPoint $mount) {
91
			return $mount instanceof SharedMount || $mount->getStorage() && $mount->getStorage()->getCache();
92
		});
93
		/** @var ICachedMountInfo[] $newMounts */
94
		$newMounts = array_map(function (IMountPoint $mount) use ($user) {
95
			// filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
96
			if ($mount->getStorageRootId() === -1) {
97
				return null;
98
			} else {
99
				return new LazyStorageMountInfo($user, $mount);
100
			}
101
		}, $mounts);
102
		$newMounts = array_values(array_filter($newMounts));
103
104
		$cachedMounts = $this->getMountsForUser($user);
105
		$mountDiff = function (ICachedMountInfo $mount1, ICachedMountInfo $mount2) {
106
			// since we are only looking for mounts for a specific user comparing on root id is enough
107
			return $mount1->getRootId() - $mount2->getRootId();
108
		};
109
110
		/** @var ICachedMountInfo[] $addedMounts */
111
		$addedMounts = array_udiff($newMounts, $cachedMounts, $mountDiff);
112
		/** @var ICachedMountInfo[] $removedMounts */
113
		$removedMounts = array_udiff($cachedMounts, $newMounts, $mountDiff);
114
115
		$changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
116
117
		foreach ($addedMounts as $mount) {
118
			$this->addToCache($mount);
119
			$this->mountsForUsers[$user->getUID()][] = $mount;
120
		}
121
		foreach ($removedMounts as $mount) {
122
			$this->removeFromCache($mount);
123
			$index = array_search($mount, $this->mountsForUsers[$user->getUID()]);
124
			unset($this->mountsForUsers[$user->getUID()][$index]);
125
		}
126
		foreach ($changedMounts as $mount) {
127
			$this->updateCachedMount($mount);
128
		}
129
	}
130
131
	/**
132
	 * @param ICachedMountInfo[] $newMounts
133
	 * @param ICachedMountInfo[] $cachedMounts
134
	 * @return ICachedMountInfo[]
135
	 */
136
	private function findChangedMounts(array $newMounts, array $cachedMounts) {
137
		$changed = [];
138
		foreach ($newMounts as $newMount) {
139
			foreach ($cachedMounts as $cachedMount) {
140
				if (
141
					$newMount->getRootId() === $cachedMount->getRootId() &&
142
					(
143
						$newMount->getMountPoint() !== $cachedMount->getMountPoint() ||
144
						$newMount->getStorageId() !== $cachedMount->getStorageId() ||
145
						$newMount->getMountId() !== $cachedMount->getMountId()
146
					)
147
				) {
148
					$changed[] = $newMount;
149
				}
150
			}
151
		}
152
		return $changed;
153
	}
154
155
	private function addToCache(ICachedMountInfo $mount) {
156
		if ($mount->getStorageId() !== -1) {
157
			$this->connection->insertIfNotExist('*PREFIX*mounts', [
158
				'storage_id' => $mount->getStorageId(),
159
				'root_id' => $mount->getRootId(),
160
				'user_id' => $mount->getUser()->getUID(),
161
				'mount_point' => $mount->getMountPoint(),
162
				'mount_id' => $mount->getMountId()
163
			], ['root_id', 'user_id']);
164
		} else {
165
			// in some cases this is legitimate, like orphaned shares
166
			$this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
167
		}
168
	}
169
170
	private function updateCachedMount(ICachedMountInfo $mount) {
171
		$builder = $this->connection->getQueryBuilder();
172
173
		$query = $builder->update('mounts')
174
			->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
175
			->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
176
			->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
177
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
178
			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
179
180
		$query->execute();
181
	}
182
183 View Code Duplication
	private function removeFromCache(ICachedMountInfo $mount) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
184
		$builder = $this->connection->getQueryBuilder();
185
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
192
	private function dbRowToMountInfo(array $row) {
193
		$user = $this->userManager->get($row['user_id']);
194
		if (is_null($user)) {
195
			return null;
196
		}
197
		return new CachedMountInfo($user, (int)$row['storage_id'], (int)$row['root_id'], $row['mount_point'], $row['mount_id'], isset($row['path']) ? $row['path'] : '');
198
	}
199
200
	/**
201
	 * @param IUser $user
202
	 * @return ICachedMountInfo[]
203
	 */
204
	public function getMountsForUser(IUser $user) {
205
		if (!isset($this->mountsForUsers[$user->getUID()])) {
206
			$builder = $this->connection->getQueryBuilder();
207
			$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
208
				->from('mounts', 'm')
209
				->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
210
				->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($user->getUID())));
211
212
			$rows = $query->execute()->fetchAll();
213
214
			$this->mountsForUsers[$user->getUID()] = array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
215
		}
216
		return $this->mountsForUsers[$user->getUID()];
217
	}
218
219
	/**
220
	 * @param int $numericStorageId
221
	 * @param string|null $user limit the results to a single user
222
	 * @return CachedMountInfo[]
223
	 */
224
	public function getMountsForStorageId($numericStorageId, $user = null) {
225
		$builder = $this->connection->getQueryBuilder();
226
		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
227
			->from('mounts', 'm')
228
			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
229
			->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
230
231
		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...
232
			$query->andWhere($builder->expr()->eq('user_id', $builder->createPositionalParameter($user)));
233
		}
234
235
		$rows = $query->execute()->fetchAll();
236
237
		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
238
	}
239
240
	/**
241
	 * @param int $rootFileId
242
	 * @return CachedMountInfo[]
243
	 */
244
	public function getMountsForRootId($rootFileId) {
245
		$builder = $this->connection->getQueryBuilder();
246
		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
247
			->from('mounts', 'm')
248
			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
249
			->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT)));
250
251
		$rows = $query->execute()->fetchAll();
252
253
		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
254
	}
255
256
	/**
257
	 * @param $fileId
258
	 * @return array
259
	 * @throws \OCP\Files\NotFoundException
260
	 */
261
	private function getCacheInfoFromFileId($fileId) {
262
		if (!isset($this->cacheInfoCache[$fileId])) {
263
			$builder = $this->connection->getQueryBuilder();
264
			$query = $builder->select('storage', 'path', 'mimetype')
265
				->from('filecache')
266
				->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
267
268
			$row = $query->execute()->fetch();
269
			if (is_array($row)) {
270
				$this->cacheInfoCache[$fileId] = [
271
					(int)$row['storage'],
272
					$row['path'],
273
					(int)$row['mimetype']
274
				];
275
			} else {
276
				throw new NotFoundException('File with id "' . $fileId . '" not found');
277
			}
278
		}
279
		return $this->cacheInfoCache[$fileId];
280
	}
281
282
	/**
283
	 * @param int $fileId
284
	 * @param string|null $user optionally restrict the results to a single user
285
	 * @return ICachedMountInfo[]
286
	 * @since 9.0.0
287
	 */
288
	public function getMountsForFileId($fileId, $user = null) {
289
		try {
290
			list($storageId, $internalPath) = $this->getCacheInfoFromFileId($fileId);
291
		} catch (NotFoundException $e) {
292
			return [];
293
		}
294
		$mountsForStorage = $this->getMountsForStorageId($storageId, $user);
295
296
		// filter mounts that are from the same storage but a different directory
297
		return array_filter($mountsForStorage, function (ICachedMountInfo $mount) use ($internalPath, $fileId) {
298
			if ($fileId === $mount->getRootId()) {
299
				return true;
300
			}
301
			$internalMountPath = $mount->getRootInternalPath();
302
303
			return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/';
304
		});
305
	}
306
307
	/**
308
	 * Remove all cached mounts for a user
309
	 *
310
	 * @param IUser $user
311
	 */
312 View Code Duplication
	public function removeUserMounts(IUser $user) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
313
		$builder = $this->connection->getQueryBuilder();
314
315
		$query = $builder->delete('mounts')
316
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
317
		$query->execute();
318
	}
319
320 View Code Duplication
	public function removeUserStorageMount($storageId, $userId) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
321
		$builder = $this->connection->getQueryBuilder();
322
323
		$query = $builder->delete('mounts')
324
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
325
			->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
326
		$query->execute();
327
	}
328
329 View Code Duplication
	public function remoteStorageMounts($storageId) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
330
		$builder = $this->connection->getQueryBuilder();
331
332
		$query = $builder->delete('mounts')
333
			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
334
		$query->execute();
335
	}
336
337
	public function getUsedSpaceForUsers(array $users) {
338
		$builder = $this->connection->getQueryBuilder();
339
340
		$slash = $builder->createNamedParameter('/');
341
342
		$mountPoint = $builder->func()->concat(
343
			$builder->func()->concat($slash, 'user_id'),
344
			$slash
345
		);
346
347
		$userIds = array_map(function (IUser $user) {
348
			return $user->getUID();
349
		}, $users);
350
351
		$query = $builder->select('m.user_id', 'f.size')
352
			->from('mounts', 'm')
353
			->innerJoin('m', 'filecache', 'f',
354
				$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...
355
					$builder->expr()->eq('m.storage_id', 'f.storage'),
356
					$builder->expr()->eq('f.path', $builder->createNamedParameter('files'))
357
				))
358
			->where($builder->expr()->eq('m.mount_point', $mountPoint))
359
			->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
360
361
		$result = $query->execute();
362
363
		return $result->fetchAll(\PDO::FETCH_KEY_PAIR);
364
	}
365
}
366