Completed
Push — master ( 5ab16f...7c8c59 )
by
unknown
28:32
created
lib/private/Files/Config/UserMountCache.php 1 patch
Indentation   +490 added lines, -490 removed lines patch added patch discarded remove patch
@@ -29,494 +29,494 @@
 block discarded – undo
29 29
  */
30 30
 class UserMountCache implements IUserMountCache {
31 31
 
32
-	/**
33
-	 * Cached mount info.
34
-	 * @var CappedMemoryCache<ICachedMountInfo[]>
35
-	 **/
36
-	private CappedMemoryCache $mountsForUsers;
37
-	/**
38
-	 * fileid => internal path mapping for cached mount info.
39
-	 * @var CappedMemoryCache<string>
40
-	 **/
41
-	private CappedMemoryCache $internalPathCache;
42
-	/** @var CappedMemoryCache<array> */
43
-	private CappedMemoryCache $cacheInfoCache;
44
-
45
-	/**
46
-	 * UserMountCache constructor.
47
-	 */
48
-	public function __construct(
49
-		private IDBConnection $connection,
50
-		private IUserManager $userManager,
51
-		private LoggerInterface $logger,
52
-		private IEventLogger $eventLogger,
53
-		private IEventDispatcher $eventDispatcher,
54
-	) {
55
-		$this->cacheInfoCache = new CappedMemoryCache();
56
-		$this->internalPathCache = new CappedMemoryCache();
57
-		$this->mountsForUsers = new CappedMemoryCache();
58
-	}
59
-
60
-	public function registerMounts(IUser $user, array $mounts, ?array $mountProviderClasses = null) {
61
-		$this->eventLogger->start('fs:setup:user:register', 'Registering mounts for user');
62
-		/** @var array<string, ICachedMountInfo> $newMounts */
63
-		$newMounts = [];
64
-		foreach ($mounts as $mount) {
65
-			// filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
66
-			if ($mount->getStorageRootId() !== -1) {
67
-				$mountInfo = new LazyStorageMountInfo($user, $mount);
68
-				$newMounts[$mountInfo->getKey()] = $mountInfo;
69
-			}
70
-		}
71
-
72
-		$cachedMounts = $this->getMountsForUser($user);
73
-		if (is_array($mountProviderClasses)) {
74
-			$cachedMounts = array_filter($cachedMounts, function (ICachedMountInfo $mountInfo) use ($mountProviderClasses, $newMounts) {
75
-				// for existing mounts that didn't have a mount provider set
76
-				// we still want the ones that map to new mounts
77
-				if ($mountInfo->getMountProvider() === '' && isset($newMounts[$mountInfo->getKey()])) {
78
-					return true;
79
-				}
80
-				return in_array($mountInfo->getMountProvider(), $mountProviderClasses);
81
-			});
82
-		}
83
-
84
-		$addedMounts = [];
85
-		$removedMounts = [];
86
-
87
-		foreach ($newMounts as $mountKey => $newMount) {
88
-			if (!isset($cachedMounts[$mountKey])) {
89
-				$addedMounts[] = $newMount;
90
-			}
91
-		}
92
-
93
-		foreach ($cachedMounts as $mountKey => $cachedMount) {
94
-			if (!isset($newMounts[$mountKey])) {
95
-				$removedMounts[] = $cachedMount;
96
-			}
97
-		}
98
-
99
-		$changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
100
-
101
-		if ($addedMounts || $removedMounts || $changedMounts) {
102
-			$this->connection->beginTransaction();
103
-			$userUID = $user->getUID();
104
-			try {
105
-				foreach ($addedMounts as $mount) {
106
-					$this->logger->debug("Adding mount '{$mount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $mount->getMountProvider()]);
107
-					$this->addToCache($mount);
108
-					/** @psalm-suppress InvalidArgument */
109
-					$this->mountsForUsers[$userUID][$mount->getKey()] = $mount;
110
-				}
111
-				foreach ($removedMounts as $mount) {
112
-					$this->logger->debug("Removing mount '{$mount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $mount->getMountProvider()]);
113
-					$this->removeFromCache($mount);
114
-					unset($this->mountsForUsers[$userUID][$mount->getKey()]);
115
-				}
116
-				foreach ($changedMounts as $mountPair) {
117
-					$newMount = $mountPair[1];
118
-					$this->logger->debug("Updating mount '{$newMount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $newMount->getMountProvider()]);
119
-					$this->updateCachedMount($newMount);
120
-					/** @psalm-suppress InvalidArgument */
121
-					$this->mountsForUsers[$userUID][$newMount->getKey()] = $newMount;
122
-				}
123
-				$this->connection->commit();
124
-			} catch (\Throwable $e) {
125
-				$this->connection->rollBack();
126
-				throw $e;
127
-			}
128
-
129
-			// Only fire events after all mounts have already been adjusted in the database.
130
-			foreach ($addedMounts as $mount) {
131
-				$this->eventDispatcher->dispatchTyped(new UserMountAddedEvent($mount));
132
-			}
133
-			foreach ($removedMounts as $mount) {
134
-				$this->eventDispatcher->dispatchTyped(new UserMountRemovedEvent($mount));
135
-			}
136
-			foreach ($changedMounts as $mountPair) {
137
-				$this->eventDispatcher->dispatchTyped(new UserMountUpdatedEvent($mountPair[0], $mountPair[1]));
138
-			}
139
-		}
140
-		$this->eventLogger->end('fs:setup:user:register');
141
-	}
142
-
143
-	/**
144
-	 * @param array<string, ICachedMountInfo> $newMounts
145
-	 * @param array<string, ICachedMountInfo> $cachedMounts
146
-	 * @return list<list{0: ICachedMountInfo, 1: ICachedMountInfo}> Pairs of old and new mounts
147
-	 */
148
-	private function findChangedMounts(array $newMounts, array $cachedMounts): array {
149
-		$changed = [];
150
-		foreach ($cachedMounts as $key => $cachedMount) {
151
-			if (isset($newMounts[$key])) {
152
-				$newMount = $newMounts[$key];
153
-				if (
154
-					$newMount->getStorageId() !== $cachedMount->getStorageId()
155
-					|| $newMount->getMountId() !== $cachedMount->getMountId()
156
-					|| $newMount->getMountProvider() !== $cachedMount->getMountProvider()
157
-				) {
158
-					$changed[] = [$cachedMount, $newMount];
159
-				}
160
-			}
161
-		}
162
-		return $changed;
163
-	}
164
-
165
-	private function addToCache(ICachedMountInfo $mount) {
166
-		if ($mount->getStorageId() !== -1) {
167
-			$this->connection->insertIfNotExist('*PREFIX*mounts', [
168
-				'storage_id' => $mount->getStorageId(),
169
-				'root_id' => $mount->getRootId(),
170
-				'user_id' => $mount->getUser()->getUID(),
171
-				'mount_point' => $mount->getMountPoint(),
172
-				'mount_id' => $mount->getMountId(),
173
-				'mount_provider_class' => $mount->getMountProvider(),
174
-			], ['root_id', 'user_id', 'mount_point']);
175
-		} else {
176
-			// in some cases this is legitimate, like orphaned shares
177
-			$this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
178
-		}
179
-	}
180
-
181
-	private function updateCachedMount(ICachedMountInfo $mount) {
182
-		$builder = $this->connection->getQueryBuilder();
183
-
184
-		$query = $builder->update('mounts')
185
-			->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
186
-			->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
187
-			->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
188
-			->set('mount_provider_class', $builder->createNamedParameter($mount->getMountProvider()))
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
-
192
-		$query->executeStatement();
193
-	}
194
-
195
-	private function removeFromCache(ICachedMountInfo $mount) {
196
-		$builder = $this->connection->getQueryBuilder();
197
-
198
-		$query = $builder->delete('mounts')
199
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
200
-			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)))
201
-			->andWhere($builder->expr()->eq('mount_point', $builder->createNamedParameter($mount->getMountPoint())));
202
-		$query->executeStatement();
203
-	}
204
-
205
-	/**
206
-	 * @param array $row
207
-	 * @param (callable(CachedMountInfo): string)|null $pathCallback
208
-	 * @return CachedMountInfo
209
-	 */
210
-	private function dbRowToMountInfo(array $row, ?callable $pathCallback = null): ICachedMountInfo {
211
-		$user = new LazyUser($row['user_id'], $this->userManager);
212
-		$mount_id = $row['mount_id'];
213
-		if (!is_null($mount_id)) {
214
-			$mount_id = (int)$mount_id;
215
-		}
216
-		if ($pathCallback) {
217
-			return new LazyPathCachedMountInfo(
218
-				$user,
219
-				(int)$row['storage_id'],
220
-				(int)$row['root_id'],
221
-				$row['mount_point'],
222
-				$row['mount_provider_class'] ?? '',
223
-				$mount_id,
224
-				$pathCallback,
225
-			);
226
-		} else {
227
-			return new CachedMountInfo(
228
-				$user,
229
-				(int)$row['storage_id'],
230
-				(int)$row['root_id'],
231
-				$row['mount_point'],
232
-				$row['mount_provider_class'] ?? '',
233
-				$mount_id,
234
-				$row['path'] ?? '',
235
-			);
236
-		}
237
-	}
238
-
239
-	/**
240
-	 * @param IUser $user
241
-	 * @return ICachedMountInfo[]
242
-	 */
243
-	public function getMountsForUser(IUser $user) {
244
-		$userUID = $user->getUID();
245
-		if (!$this->userManager->userExists($userUID)) {
246
-			return [];
247
-		}
248
-		if (!isset($this->mountsForUsers[$userUID])) {
249
-			$builder = $this->connection->getQueryBuilder();
250
-			$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'mount_provider_class')
251
-				->from('mounts', 'm')
252
-				->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userUID)));
253
-
254
-			$result = $query->executeQuery();
255
-			$rows = $result->fetchAll();
256
-			$result->closeCursor();
257
-
258
-			/** @var array<string, ICachedMountInfo> $mounts */
259
-			$mounts = [];
260
-			foreach ($rows as $row) {
261
-				$mount = $this->dbRowToMountInfo($row, [$this, 'getInternalPathForMountInfo']);
262
-				if ($mount !== null) {
263
-					$mounts[$mount->getKey()] = $mount;
264
-				}
265
-			}
266
-			$this->mountsForUsers[$userUID] = $mounts;
267
-		}
268
-		return $this->mountsForUsers[$userUID];
269
-	}
270
-
271
-	public function getInternalPathForMountInfo(CachedMountInfo $info): string {
272
-		$cached = $this->internalPathCache->get($info->getRootId());
273
-		if ($cached !== null) {
274
-			return $cached;
275
-		}
276
-		$builder = $this->connection->getQueryBuilder();
277
-		$query = $builder->select('path')
278
-			->from('filecache')
279
-			->where($builder->expr()->eq('fileid', $builder->createNamedParameter($info->getRootId())));
280
-		return $query->executeQuery()->fetchOne() ?: '';
281
-	}
282
-
283
-	/**
284
-	 * @param int $numericStorageId
285
-	 * @param string|null $user limit the results to a single user
286
-	 * @return CachedMountInfo[]
287
-	 */
288
-	public function getMountsForStorageId($numericStorageId, $user = null) {
289
-		$builder = $this->connection->getQueryBuilder();
290
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
291
-			->from('mounts', 'm')
292
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
293
-			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
294
-
295
-		if ($user) {
296
-			$query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter($user)));
297
-		}
298
-
299
-		$result = $query->executeQuery();
300
-		$rows = $result->fetchAll();
301
-		$result->closeCursor();
302
-
303
-		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
304
-	}
305
-
306
-	/**
307
-	 * @param int $rootFileId
308
-	 * @return CachedMountInfo[]
309
-	 */
310
-	public function getMountsForRootId($rootFileId) {
311
-		$builder = $this->connection->getQueryBuilder();
312
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
313
-			->from('mounts', 'm')
314
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
315
-			->where($builder->expr()->eq('root_id', $builder->createNamedParameter($rootFileId, IQueryBuilder::PARAM_INT)));
316
-
317
-		$result = $query->executeQuery();
318
-		$rows = $result->fetchAll();
319
-		$result->closeCursor();
320
-
321
-		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
322
-	}
323
-
324
-	/**
325
-	 * @param $fileId
326
-	 * @return array{int, string, int}
327
-	 * @throws \OCP\Files\NotFoundException
328
-	 */
329
-	private function getCacheInfoFromFileId($fileId): array {
330
-		if (!isset($this->cacheInfoCache[$fileId])) {
331
-			$builder = $this->connection->getQueryBuilder();
332
-			$query = $builder->select('storage', 'path', 'mimetype')
333
-				->from('filecache')
334
-				->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
335
-
336
-			$result = $query->executeQuery();
337
-			$row = $result->fetch();
338
-			$result->closeCursor();
339
-
340
-			if (is_array($row)) {
341
-				$this->cacheInfoCache[$fileId] = [
342
-					(int)$row['storage'],
343
-					(string)$row['path'],
344
-					(int)$row['mimetype']
345
-				];
346
-			} else {
347
-				throw new NotFoundException('File with id "' . $fileId . '" not found');
348
-			}
349
-		}
350
-		return $this->cacheInfoCache[$fileId];
351
-	}
352
-
353
-	/**
354
-	 * @param int $fileId
355
-	 * @param string|null $user optionally restrict the results to a single user
356
-	 * @return ICachedMountFileInfo[]
357
-	 * @since 9.0.0
358
-	 */
359
-	public function getMountsForFileId($fileId, $user = null) {
360
-		try {
361
-			[$storageId, $internalPath] = $this->getCacheInfoFromFileId($fileId);
362
-		} catch (NotFoundException $e) {
363
-			return [];
364
-		}
365
-
366
-		$builder = $this->connection->getQueryBuilder();
367
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
368
-			->from('mounts', 'm')
369
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
370
-			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
371
-			->andWhere(
372
-				$builder->expr()->orX(
373
-					$builder->expr()->eq('f.fileid', $builder->createNamedParameter($fileId)),
374
-					$builder->expr()->emptyString('f.path'),
375
-					$builder->expr()->eq(
376
-						$builder->func()->concat('f.path', $builder->createNamedParameter('/')),
377
-						$builder->func()->substring(
378
-							$builder->createNamedParameter($internalPath),
379
-							$builder->createNamedParameter(1, IQueryBuilder::PARAM_INT),
380
-							$builder->func()->add(
381
-								$builder->func()->charLength('f.path'),
382
-								$builder->createNamedParameter(1, IQueryBuilder::PARAM_INT),
383
-							),
384
-						),
385
-					),
386
-				)
387
-			);
388
-
389
-		if ($user !== null) {
390
-			$query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter($user)));
391
-		}
392
-		$result = $query->executeQuery();
393
-
394
-		$mounts = [];
395
-		while ($row = $result->fetch()) {
396
-			if ($user === null && !$this->userManager->userExists($row['user_id'])) {
397
-				continue;
398
-			}
399
-
400
-			$mounts[] = new CachedMountFileInfo(
401
-				new LazyUser($row['user_id'], $this->userManager),
402
-				(int)$row['storage_id'],
403
-				(int)$row['root_id'],
404
-				$row['mount_point'],
405
-				$row['mount_id'] === null ? null : (int)$row['mount_id'],
406
-				$row['mount_provider_class'] ?? '',
407
-				$row['path'] ?? '',
408
-				$internalPath,
409
-			);
410
-		}
411
-
412
-		return $mounts;
413
-	}
414
-
415
-	/**
416
-	 * Remove all cached mounts for a user
417
-	 *
418
-	 * @param IUser $user
419
-	 */
420
-	public function removeUserMounts(IUser $user) {
421
-		$builder = $this->connection->getQueryBuilder();
422
-
423
-		$query = $builder->delete('mounts')
424
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
425
-		$query->executeStatement();
426
-	}
427
-
428
-	public function removeUserStorageMount($storageId, $userId) {
429
-		$builder = $this->connection->getQueryBuilder();
430
-
431
-		$query = $builder->delete('mounts')
432
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
433
-			->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
434
-		$query->executeStatement();
435
-	}
436
-
437
-	public function remoteStorageMounts($storageId) {
438
-		$builder = $this->connection->getQueryBuilder();
439
-
440
-		$query = $builder->delete('mounts')
441
-			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
442
-		$query->executeStatement();
443
-	}
444
-
445
-	/**
446
-	 * @param array $users
447
-	 * @return array
448
-	 */
449
-	public function getUsedSpaceForUsers(array $users) {
450
-		$builder = $this->connection->getQueryBuilder();
451
-
452
-		$slash = $builder->createNamedParameter('/');
453
-
454
-		$mountPoint = $builder->func()->concat(
455
-			$builder->func()->concat($slash, 'user_id'),
456
-			$slash
457
-		);
458
-
459
-		$userIds = array_map(function (IUser $user) {
460
-			return $user->getUID();
461
-		}, $users);
462
-
463
-		$query = $builder->select('m.user_id', 'f.size')
464
-			->from('mounts', 'm')
465
-			->innerJoin('m', 'filecache', 'f',
466
-				$builder->expr()->andX(
467
-					$builder->expr()->eq('m.storage_id', 'f.storage'),
468
-					$builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files')))
469
-				))
470
-			->where($builder->expr()->eq('m.mount_point', $mountPoint))
471
-			->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
472
-
473
-		$result = $query->executeQuery();
474
-
475
-		$results = [];
476
-		while ($row = $result->fetch()) {
477
-			$results[$row['user_id']] = $row['size'];
478
-		}
479
-		$result->closeCursor();
480
-		return $results;
481
-	}
482
-
483
-	public function clear(): void {
484
-		$this->cacheInfoCache = new CappedMemoryCache();
485
-		$this->mountsForUsers = new CappedMemoryCache();
486
-	}
487
-
488
-	public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
489
-		$mounts = $this->getMountsForUser($user);
490
-		$mountPoints = array_map(function (ICachedMountInfo $mount) {
491
-			return $mount->getMountPoint();
492
-		}, $mounts);
493
-		$mounts = array_combine($mountPoints, $mounts);
494
-
495
-		$current = rtrim($path, '/');
496
-		// walk up the directory tree until we find a path that has a mountpoint set
497
-		// the loop will return if a mountpoint is found or break if none are found
498
-		while (true) {
499
-			$mountPoint = $current . '/';
500
-			if (isset($mounts[$mountPoint])) {
501
-				return $mounts[$mountPoint];
502
-			} elseif ($current === '') {
503
-				break;
504
-			}
505
-
506
-			$current = dirname($current);
507
-			if ($current === '.' || $current === '/') {
508
-				$current = '';
509
-			}
510
-		}
511
-
512
-		throw new NotFoundException('No cached mount for path ' . $path);
513
-	}
514
-
515
-	public function getMountsInPath(IUser $user, string $path): array {
516
-		$path = rtrim($path, '/') . '/';
517
-		$mounts = $this->getMountsForUser($user);
518
-		return array_filter($mounts, function (ICachedMountInfo $mount) use ($path) {
519
-			return $mount->getMountPoint() !== $path && str_starts_with($mount->getMountPoint(), $path);
520
-		});
521
-	}
32
+    /**
33
+     * Cached mount info.
34
+     * @var CappedMemoryCache<ICachedMountInfo[]>
35
+     **/
36
+    private CappedMemoryCache $mountsForUsers;
37
+    /**
38
+     * fileid => internal path mapping for cached mount info.
39
+     * @var CappedMemoryCache<string>
40
+     **/
41
+    private CappedMemoryCache $internalPathCache;
42
+    /** @var CappedMemoryCache<array> */
43
+    private CappedMemoryCache $cacheInfoCache;
44
+
45
+    /**
46
+     * UserMountCache constructor.
47
+     */
48
+    public function __construct(
49
+        private IDBConnection $connection,
50
+        private IUserManager $userManager,
51
+        private LoggerInterface $logger,
52
+        private IEventLogger $eventLogger,
53
+        private IEventDispatcher $eventDispatcher,
54
+    ) {
55
+        $this->cacheInfoCache = new CappedMemoryCache();
56
+        $this->internalPathCache = new CappedMemoryCache();
57
+        $this->mountsForUsers = new CappedMemoryCache();
58
+    }
59
+
60
+    public function registerMounts(IUser $user, array $mounts, ?array $mountProviderClasses = null) {
61
+        $this->eventLogger->start('fs:setup:user:register', 'Registering mounts for user');
62
+        /** @var array<string, ICachedMountInfo> $newMounts */
63
+        $newMounts = [];
64
+        foreach ($mounts as $mount) {
65
+            // filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
66
+            if ($mount->getStorageRootId() !== -1) {
67
+                $mountInfo = new LazyStorageMountInfo($user, $mount);
68
+                $newMounts[$mountInfo->getKey()] = $mountInfo;
69
+            }
70
+        }
71
+
72
+        $cachedMounts = $this->getMountsForUser($user);
73
+        if (is_array($mountProviderClasses)) {
74
+            $cachedMounts = array_filter($cachedMounts, function (ICachedMountInfo $mountInfo) use ($mountProviderClasses, $newMounts) {
75
+                // for existing mounts that didn't have a mount provider set
76
+                // we still want the ones that map to new mounts
77
+                if ($mountInfo->getMountProvider() === '' && isset($newMounts[$mountInfo->getKey()])) {
78
+                    return true;
79
+                }
80
+                return in_array($mountInfo->getMountProvider(), $mountProviderClasses);
81
+            });
82
+        }
83
+
84
+        $addedMounts = [];
85
+        $removedMounts = [];
86
+
87
+        foreach ($newMounts as $mountKey => $newMount) {
88
+            if (!isset($cachedMounts[$mountKey])) {
89
+                $addedMounts[] = $newMount;
90
+            }
91
+        }
92
+
93
+        foreach ($cachedMounts as $mountKey => $cachedMount) {
94
+            if (!isset($newMounts[$mountKey])) {
95
+                $removedMounts[] = $cachedMount;
96
+            }
97
+        }
98
+
99
+        $changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
100
+
101
+        if ($addedMounts || $removedMounts || $changedMounts) {
102
+            $this->connection->beginTransaction();
103
+            $userUID = $user->getUID();
104
+            try {
105
+                foreach ($addedMounts as $mount) {
106
+                    $this->logger->debug("Adding mount '{$mount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $mount->getMountProvider()]);
107
+                    $this->addToCache($mount);
108
+                    /** @psalm-suppress InvalidArgument */
109
+                    $this->mountsForUsers[$userUID][$mount->getKey()] = $mount;
110
+                }
111
+                foreach ($removedMounts as $mount) {
112
+                    $this->logger->debug("Removing mount '{$mount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $mount->getMountProvider()]);
113
+                    $this->removeFromCache($mount);
114
+                    unset($this->mountsForUsers[$userUID][$mount->getKey()]);
115
+                }
116
+                foreach ($changedMounts as $mountPair) {
117
+                    $newMount = $mountPair[1];
118
+                    $this->logger->debug("Updating mount '{$newMount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $newMount->getMountProvider()]);
119
+                    $this->updateCachedMount($newMount);
120
+                    /** @psalm-suppress InvalidArgument */
121
+                    $this->mountsForUsers[$userUID][$newMount->getKey()] = $newMount;
122
+                }
123
+                $this->connection->commit();
124
+            } catch (\Throwable $e) {
125
+                $this->connection->rollBack();
126
+                throw $e;
127
+            }
128
+
129
+            // Only fire events after all mounts have already been adjusted in the database.
130
+            foreach ($addedMounts as $mount) {
131
+                $this->eventDispatcher->dispatchTyped(new UserMountAddedEvent($mount));
132
+            }
133
+            foreach ($removedMounts as $mount) {
134
+                $this->eventDispatcher->dispatchTyped(new UserMountRemovedEvent($mount));
135
+            }
136
+            foreach ($changedMounts as $mountPair) {
137
+                $this->eventDispatcher->dispatchTyped(new UserMountUpdatedEvent($mountPair[0], $mountPair[1]));
138
+            }
139
+        }
140
+        $this->eventLogger->end('fs:setup:user:register');
141
+    }
142
+
143
+    /**
144
+     * @param array<string, ICachedMountInfo> $newMounts
145
+     * @param array<string, ICachedMountInfo> $cachedMounts
146
+     * @return list<list{0: ICachedMountInfo, 1: ICachedMountInfo}> Pairs of old and new mounts
147
+     */
148
+    private function findChangedMounts(array $newMounts, array $cachedMounts): array {
149
+        $changed = [];
150
+        foreach ($cachedMounts as $key => $cachedMount) {
151
+            if (isset($newMounts[$key])) {
152
+                $newMount = $newMounts[$key];
153
+                if (
154
+                    $newMount->getStorageId() !== $cachedMount->getStorageId()
155
+                    || $newMount->getMountId() !== $cachedMount->getMountId()
156
+                    || $newMount->getMountProvider() !== $cachedMount->getMountProvider()
157
+                ) {
158
+                    $changed[] = [$cachedMount, $newMount];
159
+                }
160
+            }
161
+        }
162
+        return $changed;
163
+    }
164
+
165
+    private function addToCache(ICachedMountInfo $mount) {
166
+        if ($mount->getStorageId() !== -1) {
167
+            $this->connection->insertIfNotExist('*PREFIX*mounts', [
168
+                'storage_id' => $mount->getStorageId(),
169
+                'root_id' => $mount->getRootId(),
170
+                'user_id' => $mount->getUser()->getUID(),
171
+                'mount_point' => $mount->getMountPoint(),
172
+                'mount_id' => $mount->getMountId(),
173
+                'mount_provider_class' => $mount->getMountProvider(),
174
+            ], ['root_id', 'user_id', 'mount_point']);
175
+        } else {
176
+            // in some cases this is legitimate, like orphaned shares
177
+            $this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
178
+        }
179
+    }
180
+
181
+    private function updateCachedMount(ICachedMountInfo $mount) {
182
+        $builder = $this->connection->getQueryBuilder();
183
+
184
+        $query = $builder->update('mounts')
185
+            ->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
186
+            ->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
187
+            ->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
188
+            ->set('mount_provider_class', $builder->createNamedParameter($mount->getMountProvider()))
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
+
192
+        $query->executeStatement();
193
+    }
194
+
195
+    private function removeFromCache(ICachedMountInfo $mount) {
196
+        $builder = $this->connection->getQueryBuilder();
197
+
198
+        $query = $builder->delete('mounts')
199
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
200
+            ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)))
201
+            ->andWhere($builder->expr()->eq('mount_point', $builder->createNamedParameter($mount->getMountPoint())));
202
+        $query->executeStatement();
203
+    }
204
+
205
+    /**
206
+     * @param array $row
207
+     * @param (callable(CachedMountInfo): string)|null $pathCallback
208
+     * @return CachedMountInfo
209
+     */
210
+    private function dbRowToMountInfo(array $row, ?callable $pathCallback = null): ICachedMountInfo {
211
+        $user = new LazyUser($row['user_id'], $this->userManager);
212
+        $mount_id = $row['mount_id'];
213
+        if (!is_null($mount_id)) {
214
+            $mount_id = (int)$mount_id;
215
+        }
216
+        if ($pathCallback) {
217
+            return new LazyPathCachedMountInfo(
218
+                $user,
219
+                (int)$row['storage_id'],
220
+                (int)$row['root_id'],
221
+                $row['mount_point'],
222
+                $row['mount_provider_class'] ?? '',
223
+                $mount_id,
224
+                $pathCallback,
225
+            );
226
+        } else {
227
+            return new CachedMountInfo(
228
+                $user,
229
+                (int)$row['storage_id'],
230
+                (int)$row['root_id'],
231
+                $row['mount_point'],
232
+                $row['mount_provider_class'] ?? '',
233
+                $mount_id,
234
+                $row['path'] ?? '',
235
+            );
236
+        }
237
+    }
238
+
239
+    /**
240
+     * @param IUser $user
241
+     * @return ICachedMountInfo[]
242
+     */
243
+    public function getMountsForUser(IUser $user) {
244
+        $userUID = $user->getUID();
245
+        if (!$this->userManager->userExists($userUID)) {
246
+            return [];
247
+        }
248
+        if (!isset($this->mountsForUsers[$userUID])) {
249
+            $builder = $this->connection->getQueryBuilder();
250
+            $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'mount_provider_class')
251
+                ->from('mounts', 'm')
252
+                ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userUID)));
253
+
254
+            $result = $query->executeQuery();
255
+            $rows = $result->fetchAll();
256
+            $result->closeCursor();
257
+
258
+            /** @var array<string, ICachedMountInfo> $mounts */
259
+            $mounts = [];
260
+            foreach ($rows as $row) {
261
+                $mount = $this->dbRowToMountInfo($row, [$this, 'getInternalPathForMountInfo']);
262
+                if ($mount !== null) {
263
+                    $mounts[$mount->getKey()] = $mount;
264
+                }
265
+            }
266
+            $this->mountsForUsers[$userUID] = $mounts;
267
+        }
268
+        return $this->mountsForUsers[$userUID];
269
+    }
270
+
271
+    public function getInternalPathForMountInfo(CachedMountInfo $info): string {
272
+        $cached = $this->internalPathCache->get($info->getRootId());
273
+        if ($cached !== null) {
274
+            return $cached;
275
+        }
276
+        $builder = $this->connection->getQueryBuilder();
277
+        $query = $builder->select('path')
278
+            ->from('filecache')
279
+            ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($info->getRootId())));
280
+        return $query->executeQuery()->fetchOne() ?: '';
281
+    }
282
+
283
+    /**
284
+     * @param int $numericStorageId
285
+     * @param string|null $user limit the results to a single user
286
+     * @return CachedMountInfo[]
287
+     */
288
+    public function getMountsForStorageId($numericStorageId, $user = null) {
289
+        $builder = $this->connection->getQueryBuilder();
290
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
291
+            ->from('mounts', 'm')
292
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
293
+            ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
294
+
295
+        if ($user) {
296
+            $query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter($user)));
297
+        }
298
+
299
+        $result = $query->executeQuery();
300
+        $rows = $result->fetchAll();
301
+        $result->closeCursor();
302
+
303
+        return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
304
+    }
305
+
306
+    /**
307
+     * @param int $rootFileId
308
+     * @return CachedMountInfo[]
309
+     */
310
+    public function getMountsForRootId($rootFileId) {
311
+        $builder = $this->connection->getQueryBuilder();
312
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
313
+            ->from('mounts', 'm')
314
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
315
+            ->where($builder->expr()->eq('root_id', $builder->createNamedParameter($rootFileId, IQueryBuilder::PARAM_INT)));
316
+
317
+        $result = $query->executeQuery();
318
+        $rows = $result->fetchAll();
319
+        $result->closeCursor();
320
+
321
+        return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
322
+    }
323
+
324
+    /**
325
+     * @param $fileId
326
+     * @return array{int, string, int}
327
+     * @throws \OCP\Files\NotFoundException
328
+     */
329
+    private function getCacheInfoFromFileId($fileId): array {
330
+        if (!isset($this->cacheInfoCache[$fileId])) {
331
+            $builder = $this->connection->getQueryBuilder();
332
+            $query = $builder->select('storage', 'path', 'mimetype')
333
+                ->from('filecache')
334
+                ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
335
+
336
+            $result = $query->executeQuery();
337
+            $row = $result->fetch();
338
+            $result->closeCursor();
339
+
340
+            if (is_array($row)) {
341
+                $this->cacheInfoCache[$fileId] = [
342
+                    (int)$row['storage'],
343
+                    (string)$row['path'],
344
+                    (int)$row['mimetype']
345
+                ];
346
+            } else {
347
+                throw new NotFoundException('File with id "' . $fileId . '" not found');
348
+            }
349
+        }
350
+        return $this->cacheInfoCache[$fileId];
351
+    }
352
+
353
+    /**
354
+     * @param int $fileId
355
+     * @param string|null $user optionally restrict the results to a single user
356
+     * @return ICachedMountFileInfo[]
357
+     * @since 9.0.0
358
+     */
359
+    public function getMountsForFileId($fileId, $user = null) {
360
+        try {
361
+            [$storageId, $internalPath] = $this->getCacheInfoFromFileId($fileId);
362
+        } catch (NotFoundException $e) {
363
+            return [];
364
+        }
365
+
366
+        $builder = $this->connection->getQueryBuilder();
367
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
368
+            ->from('mounts', 'm')
369
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
370
+            ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
371
+            ->andWhere(
372
+                $builder->expr()->orX(
373
+                    $builder->expr()->eq('f.fileid', $builder->createNamedParameter($fileId)),
374
+                    $builder->expr()->emptyString('f.path'),
375
+                    $builder->expr()->eq(
376
+                        $builder->func()->concat('f.path', $builder->createNamedParameter('/')),
377
+                        $builder->func()->substring(
378
+                            $builder->createNamedParameter($internalPath),
379
+                            $builder->createNamedParameter(1, IQueryBuilder::PARAM_INT),
380
+                            $builder->func()->add(
381
+                                $builder->func()->charLength('f.path'),
382
+                                $builder->createNamedParameter(1, IQueryBuilder::PARAM_INT),
383
+                            ),
384
+                        ),
385
+                    ),
386
+                )
387
+            );
388
+
389
+        if ($user !== null) {
390
+            $query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter($user)));
391
+        }
392
+        $result = $query->executeQuery();
393
+
394
+        $mounts = [];
395
+        while ($row = $result->fetch()) {
396
+            if ($user === null && !$this->userManager->userExists($row['user_id'])) {
397
+                continue;
398
+            }
399
+
400
+            $mounts[] = new CachedMountFileInfo(
401
+                new LazyUser($row['user_id'], $this->userManager),
402
+                (int)$row['storage_id'],
403
+                (int)$row['root_id'],
404
+                $row['mount_point'],
405
+                $row['mount_id'] === null ? null : (int)$row['mount_id'],
406
+                $row['mount_provider_class'] ?? '',
407
+                $row['path'] ?? '',
408
+                $internalPath,
409
+            );
410
+        }
411
+
412
+        return $mounts;
413
+    }
414
+
415
+    /**
416
+     * Remove all cached mounts for a user
417
+     *
418
+     * @param IUser $user
419
+     */
420
+    public function removeUserMounts(IUser $user) {
421
+        $builder = $this->connection->getQueryBuilder();
422
+
423
+        $query = $builder->delete('mounts')
424
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
425
+        $query->executeStatement();
426
+    }
427
+
428
+    public function removeUserStorageMount($storageId, $userId) {
429
+        $builder = $this->connection->getQueryBuilder();
430
+
431
+        $query = $builder->delete('mounts')
432
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
433
+            ->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
434
+        $query->executeStatement();
435
+    }
436
+
437
+    public function remoteStorageMounts($storageId) {
438
+        $builder = $this->connection->getQueryBuilder();
439
+
440
+        $query = $builder->delete('mounts')
441
+            ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
442
+        $query->executeStatement();
443
+    }
444
+
445
+    /**
446
+     * @param array $users
447
+     * @return array
448
+     */
449
+    public function getUsedSpaceForUsers(array $users) {
450
+        $builder = $this->connection->getQueryBuilder();
451
+
452
+        $slash = $builder->createNamedParameter('/');
453
+
454
+        $mountPoint = $builder->func()->concat(
455
+            $builder->func()->concat($slash, 'user_id'),
456
+            $slash
457
+        );
458
+
459
+        $userIds = array_map(function (IUser $user) {
460
+            return $user->getUID();
461
+        }, $users);
462
+
463
+        $query = $builder->select('m.user_id', 'f.size')
464
+            ->from('mounts', 'm')
465
+            ->innerJoin('m', 'filecache', 'f',
466
+                $builder->expr()->andX(
467
+                    $builder->expr()->eq('m.storage_id', 'f.storage'),
468
+                    $builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files')))
469
+                ))
470
+            ->where($builder->expr()->eq('m.mount_point', $mountPoint))
471
+            ->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
472
+
473
+        $result = $query->executeQuery();
474
+
475
+        $results = [];
476
+        while ($row = $result->fetch()) {
477
+            $results[$row['user_id']] = $row['size'];
478
+        }
479
+        $result->closeCursor();
480
+        return $results;
481
+    }
482
+
483
+    public function clear(): void {
484
+        $this->cacheInfoCache = new CappedMemoryCache();
485
+        $this->mountsForUsers = new CappedMemoryCache();
486
+    }
487
+
488
+    public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
489
+        $mounts = $this->getMountsForUser($user);
490
+        $mountPoints = array_map(function (ICachedMountInfo $mount) {
491
+            return $mount->getMountPoint();
492
+        }, $mounts);
493
+        $mounts = array_combine($mountPoints, $mounts);
494
+
495
+        $current = rtrim($path, '/');
496
+        // walk up the directory tree until we find a path that has a mountpoint set
497
+        // the loop will return if a mountpoint is found or break if none are found
498
+        while (true) {
499
+            $mountPoint = $current . '/';
500
+            if (isset($mounts[$mountPoint])) {
501
+                return $mounts[$mountPoint];
502
+            } elseif ($current === '') {
503
+                break;
504
+            }
505
+
506
+            $current = dirname($current);
507
+            if ($current === '.' || $current === '/') {
508
+                $current = '';
509
+            }
510
+        }
511
+
512
+        throw new NotFoundException('No cached mount for path ' . $path);
513
+    }
514
+
515
+    public function getMountsInPath(IUser $user, string $path): array {
516
+        $path = rtrim($path, '/') . '/';
517
+        $mounts = $this->getMountsForUser($user);
518
+        return array_filter($mounts, function (ICachedMountInfo $mount) use ($path) {
519
+            return $mount->getMountPoint() !== $path && str_starts_with($mount->getMountPoint(), $path);
520
+        });
521
+    }
522 522
 }
Please login to merge, or discard this patch.