Completed
Push — master ( 53eb0f...62e19d )
by Lukas
35:26 queued 21:10
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 1
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 Doctrine\DBAL\Exception\UniqueConstraintViolationException;
28
use OC\Files\Filesystem;
29
use OCA\Files_Sharing\SharedMount;
30
use OCP\DB\QueryBuilder\IQueryBuilder;
31
use OCP\Files\Config\ICachedMountInfo;
32
use OCP\Files\Config\IUserMountCache;
33
use OCP\Files\Mount\IMountPoint;
34
use OCP\Files\Node;
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
					($newMount->getMountPoint() !== $cachedMount->getMountPoint() || $newMount->getMountId() !== $cachedMount->getMountId())
145
				) {
146
					$changed[] = $newMount;
147
				}
148
			}
149
		}
150
		return $changed;
151
	}
152
153
	private function addToCache(ICachedMountInfo $mount) {
154
		if ($mount->getStorageId() !== -1) {
155
			$this->connection->insertIfNotExist('*PREFIX*mounts', [
156
				'storage_id' => $mount->getStorageId(),
157
				'root_id' => $mount->getRootId(),
158
				'user_id' => $mount->getUser()->getUID(),
159
				'mount_point' => $mount->getMountPoint(),
160
				'mount_id' => $mount->getMountId()
161
			], ['root_id', 'user_id']);
162
		} else {
163
			// in some cases this is legitimate, like orphaned shares
164
			$this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
165
		}
166
	}
167
168
	private function updateCachedMount(ICachedMountInfo $mount) {
169
		$builder = $this->connection->getQueryBuilder();
170
171
		$query = $builder->update('mounts')
172
			->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
173
			->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
174
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
175
			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
176
177
		$query->execute();
178
	}
179
180 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...
181
		$builder = $this->connection->getQueryBuilder();
182
183
		$query = $builder->delete('mounts')
184
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
185
			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
186
		$query->execute();
187
	}
188
189
	private function dbRowToMountInfo(array $row) {
190
		$user = $this->userManager->get($row['user_id']);
191
		return new CachedMountInfo($user, (int)$row['storage_id'], (int)$row['root_id'], $row['mount_point'], $row['mount_id'], isset($row['path'])? $row['path']:'');
0 ignored issues
show
Bug introduced by
It seems like $user defined by $this->userManager->get($row['user_id']) on line 190 can be null; however, OC\Files\Config\CachedMountInfo::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
192
	}
193
194
	/**
195
	 * @param IUser $user
196
	 * @return ICachedMountInfo[]
197
	 */
198
	public function getMountsForUser(IUser $user) {
199
		if (!isset($this->mountsForUsers[$user->getUID()])) {
200
			$builder = $this->connection->getQueryBuilder();
201
			$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
0 ignored issues
show
Unused Code introduced by
The call to IQueryBuilder::select() has too many arguments starting with 'root_id'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
202
				->from('mounts', 'm')
203
				->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
204
				->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($user->getUID())));
205
206
			$rows = $query->execute()->fetchAll();
207
208
			$this->mountsForUsers[$user->getUID()] = array_map([$this, 'dbRowToMountInfo'], $rows);
209
		}
210
		return $this->mountsForUsers[$user->getUID()];
211
	}
212
213
	/**
214
	 * @param int $numericStorageId
215
	 * @return CachedMountInfo[]
216
	 */
217 View Code Duplication
	public function getMountsForStorageId($numericStorageId) {
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...
218
		$builder = $this->connection->getQueryBuilder();
219
		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
0 ignored issues
show
Unused Code introduced by
The call to IQueryBuilder::select() has too many arguments starting with 'root_id'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
220
			->from('mounts', 'm')
221
			->innerJoin('m', 'filecache', 'f' , $builder->expr()->eq('m.root_id', 'f.fileid'))
222
			->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
223
224
		$rows = $query->execute()->fetchAll();
225
226
		return array_map([$this, 'dbRowToMountInfo'], $rows);
227
	}
228
229
	/**
230
	 * @param int $rootFileId
231
	 * @return CachedMountInfo[]
232
	 */
233 View Code Duplication
	public function getMountsForRootId($rootFileId) {
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...
234
		$builder = $this->connection->getQueryBuilder();
235
		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
0 ignored issues
show
Unused Code introduced by
The call to IQueryBuilder::select() has too many arguments starting with 'root_id'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
236
			->from('mounts', 'm')
237
			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
238
			->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT)));
239
240
		$rows = $query->execute()->fetchAll();
241
242
		return array_map([$this, 'dbRowToMountInfo'], $rows);
243
	}
244
245
	/**
246
	 * @param $fileId
247
	 * @return array
248
	 * @throws \OCP\Files\NotFoundException
249
	 */
250
	private function getCacheInfoFromFileId($fileId) {
251
		if (!isset($this->cacheInfoCache[$fileId])) {
252
			$builder = $this->connection->getQueryBuilder();
253
			$query = $builder->select('storage', 'path', 'mimetype')
0 ignored issues
show
Unused Code introduced by
The call to IQueryBuilder::select() has too many arguments starting with 'path'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
254
				->from('filecache')
255
				->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
256
257
			$row = $query->execute()->fetch();
258
			if (is_array($row)) {
259
				$this->cacheInfoCache[$fileId] = [
260
					(int)$row['storage'],
261
					$row['path'],
262
					(int)$row['mimetype']
263
				];
264
			} else {
265
				throw new NotFoundException('File with id "' . $fileId . '" not found');
266
			}
267
		}
268
		return $this->cacheInfoCache[$fileId];
269
	}
270
271
	/**
272
	 * @param int $fileId
273
	 * @return ICachedMountInfo[]
274
	 * @since 9.0.0
275
	 */
276
	public function getMountsForFileId($fileId) {
277
		try {
278
			list($storageId, $internalPath) = $this->getCacheInfoFromFileId($fileId);
279
		} catch (NotFoundException $e) {
280
			return [];
281
		}
282
		$mountsForStorage = $this->getMountsForStorageId($storageId);
283
284
		// filter mounts that are from the same storage but a different directory
285
		return array_filter($mountsForStorage, function (ICachedMountInfo $mount) use ($internalPath, $fileId) {
286
			if ($fileId === $mount->getRootId()) {
287
				return true;
288
			}
289
			$internalMountPath = $mount->getRootInternalPath();
290
291
			return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/';
292
		});
293
	}
294
295
	/**
296
	 * Remove all cached mounts for a user
297
	 *
298
	 * @param IUser $user
299
	 */
300 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...
301
		$builder = $this->connection->getQueryBuilder();
302
303
		$query = $builder->delete('mounts')
304
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
305
		$query->execute();
306
	}
307
308
	public function removeUserStorageMount($storageId, $userId) {
309
		$builder = $this->connection->getQueryBuilder();
310
311
		$query = $builder->delete('mounts')
312
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
313
			->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
314
		$query->execute();
315
	}
316
317 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...
318
		$builder = $this->connection->getQueryBuilder();
319
320
		$query = $builder->delete('mounts')
321
			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
322
		$query->execute();
323
	}
324
}
325