Completed
Push — master ( 3b3ab2...edd01e )
by Lukas
70:42 queued 62:17
created

UserMountCache::registerMounts()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 42
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 25
nc 8
nop 2
dl 0
loc 42
rs 6.7272
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
					(
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) {
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...
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
		return new CachedMountInfo($user, (int)$row['storage_id'], (int)$row['root_id'], $row['mount_point'], $row['mount_id'], isset($row['path'])? $row['path']:'');
200
	}
201
202
	/**
203
	 * @param IUser $user
204
	 * @return ICachedMountInfo[]
205
	 */
206
	public function getMountsForUser(IUser $user) {
207
		if (!isset($this->mountsForUsers[$user->getUID()])) {
208
			$builder = $this->connection->getQueryBuilder();
209
			$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
210
				->from('mounts', 'm')
211
				->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
212
				->where($builder->expr()->eq('user_id', $builder->createPositionalParameter($user->getUID())));
213
214
			$rows = $query->execute()->fetchAll();
215
216
			$this->mountsForUsers[$user->getUID()] = array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
217
		}
218
		return $this->mountsForUsers[$user->getUID()];
219
	}
220
221
	/**
222
	 * @param int $numericStorageId
223
	 * @return CachedMountInfo[]
224
	 */
225 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...
226
		$builder = $this->connection->getQueryBuilder();
227
		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
228
			->from('mounts', 'm')
229
			->innerJoin('m', 'filecache', 'f' , $builder->expr()->eq('m.root_id', 'f.fileid'))
230
			->where($builder->expr()->eq('storage_id', $builder->createPositionalParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
231
232
		$rows = $query->execute()->fetchAll();
233
234
		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
235
	}
236
237
	/**
238
	 * @param int $rootFileId
239
	 * @return CachedMountInfo[]
240
	 */
241 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...
242
		$builder = $this->connection->getQueryBuilder();
243
		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path')
244
			->from('mounts', 'm')
245
			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
246
			->where($builder->expr()->eq('root_id', $builder->createPositionalParameter($rootFileId, IQueryBuilder::PARAM_INT)));
247
248
		$rows = $query->execute()->fetchAll();
249
250
		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
251
	}
252
253
	/**
254
	 * @param $fileId
255
	 * @return array
256
	 * @throws \OCP\Files\NotFoundException
257
	 */
258
	private function getCacheInfoFromFileId($fileId) {
259
		if (!isset($this->cacheInfoCache[$fileId])) {
260
			$builder = $this->connection->getQueryBuilder();
261
			$query = $builder->select('storage', 'path', 'mimetype')
262
				->from('filecache')
263
				->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
264
265
			$row = $query->execute()->fetch();
266
			if (is_array($row)) {
267
				$this->cacheInfoCache[$fileId] = [
268
					(int)$row['storage'],
269
					$row['path'],
270
					(int)$row['mimetype']
271
				];
272
			} else {
273
				throw new NotFoundException('File with id "' . $fileId . '" not found');
274
			}
275
		}
276
		return $this->cacheInfoCache[$fileId];
277
	}
278
279
	/**
280
	 * @param int $fileId
281
	 * @return ICachedMountInfo[]
282
	 * @since 9.0.0
283
	 */
284
	public function getMountsForFileId($fileId) {
285
		try {
286
			list($storageId, $internalPath) = $this->getCacheInfoFromFileId($fileId);
287
		} catch (NotFoundException $e) {
288
			return [];
289
		}
290
		$mountsForStorage = $this->getMountsForStorageId($storageId);
291
292
		// filter mounts that are from the same storage but a different directory
293
		return array_filter($mountsForStorage, function (ICachedMountInfo $mount) use ($internalPath, $fileId) {
294
			if ($fileId === $mount->getRootId()) {
295
				return true;
296
			}
297
			$internalMountPath = $mount->getRootInternalPath();
298
299
			return $internalMountPath === '' || substr($internalPath, 0, strlen($internalMountPath) + 1) === $internalMountPath . '/';
300
		});
301
	}
302
303
	/**
304
	 * Remove all cached mounts for a user
305
	 *
306
	 * @param IUser $user
307
	 */
308 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...
309
		$builder = $this->connection->getQueryBuilder();
310
311
		$query = $builder->delete('mounts')
312
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
313
		$query->execute();
314
	}
315
316 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...
317
		$builder = $this->connection->getQueryBuilder();
318
319
		$query = $builder->delete('mounts')
320
			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
321
			->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
322
		$query->execute();
323
	}
324
325 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...
326
		$builder = $this->connection->getQueryBuilder();
327
328
		$query = $builder->delete('mounts')
329
			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
330
		$query->execute();
331
	}
332
}
333