Completed
Push — master ( 5f6e6b...182926 )
by
unknown
51:17 queued 15:41
created
lib/private/Files/Config/UserMountCache.php 1 patch
Indentation   +494 added lines, -494 removed lines patch added patch discarded remove patch
@@ -30,498 +30,498 @@
 block discarded – undo
30 30
  */
31 31
 class UserMountCache implements IUserMountCache {
32 32
 
33
-	/**
34
-	 * Cached mount info.
35
-	 * @var CappedMemoryCache<ICachedMountInfo[]>
36
-	 **/
37
-	private CappedMemoryCache $mountsForUsers;
38
-	/**
39
-	 * fileid => internal path mapping for cached mount info.
40
-	 * @var CappedMemoryCache<string>
41
-	 **/
42
-	private CappedMemoryCache $internalPathCache;
43
-	/** @var CappedMemoryCache<array> */
44
-	private CappedMemoryCache $cacheInfoCache;
45
-
46
-	/**
47
-	 * UserMountCache constructor.
48
-	 */
49
-	public function __construct(
50
-		private IDBConnection $connection,
51
-		private IUserManager $userManager,
52
-		private LoggerInterface $logger,
53
-		private IEventLogger $eventLogger,
54
-		private IEventDispatcher $eventDispatcher,
55
-	) {
56
-		$this->cacheInfoCache = new CappedMemoryCache();
57
-		$this->internalPathCache = new CappedMemoryCache();
58
-		$this->mountsForUsers = new CappedMemoryCache();
59
-	}
60
-
61
-	public function registerMounts(IUser $user, array $mounts, ?array $mountProviderClasses = null) {
62
-		$this->eventLogger->start('fs:setup:user:register', 'Registering mounts for user');
63
-		/** @var array<string, ICachedMountInfo> $newMounts */
64
-		$newMounts = [];
65
-		foreach ($mounts as $mount) {
66
-			// filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
67
-			if ($mount->getStorageRootId() !== -1) {
68
-				$mountInfo = new LazyStorageMountInfo($user, $mount);
69
-				$newMounts[$mountInfo->getKey()] = $mountInfo;
70
-			}
71
-		}
72
-
73
-		$cachedMounts = $this->getMountsForUser($user);
74
-		if (is_array($mountProviderClasses)) {
75
-			$cachedMounts = array_filter($cachedMounts, function (ICachedMountInfo $mountInfo) use ($mountProviderClasses, $newMounts) {
76
-				// for existing mounts that didn't have a mount provider set
77
-				// we still want the ones that map to new mounts
78
-				if ($mountInfo->getMountProvider() === '' && isset($newMounts[$mountInfo->getKey()])) {
79
-					return true;
80
-				}
81
-				return in_array($mountInfo->getMountProvider(), $mountProviderClasses);
82
-			});
83
-		}
84
-
85
-		$addedMounts = [];
86
-		$removedMounts = [];
87
-
88
-		foreach ($newMounts as $mountKey => $newMount) {
89
-			if (!isset($cachedMounts[$mountKey])) {
90
-				$addedMounts[] = $newMount;
91
-			}
92
-		}
93
-
94
-		foreach ($cachedMounts as $mountKey => $cachedMount) {
95
-			if (!isset($newMounts[$mountKey])) {
96
-				$removedMounts[] = $cachedMount;
97
-			}
98
-		}
99
-
100
-		$changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
101
-
102
-		if ($addedMounts || $removedMounts || $changedMounts) {
103
-			$this->connection->beginTransaction();
104
-			$userUID = $user->getUID();
105
-			try {
106
-				foreach ($addedMounts as $mount) {
107
-					$this->logger->debug("Adding mount '{$mount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $mount->getMountProvider()]);
108
-					$this->addToCache($mount);
109
-					/** @psalm-suppress InvalidArgument */
110
-					$this->mountsForUsers[$userUID][$mount->getKey()] = $mount;
111
-				}
112
-				foreach ($removedMounts as $mount) {
113
-					$this->logger->debug("Removing mount '{$mount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $mount->getMountProvider()]);
114
-					$this->removeFromCache($mount);
115
-					unset($this->mountsForUsers[$userUID][$mount->getKey()]);
116
-				}
117
-				foreach ($changedMounts as $mountPair) {
118
-					$newMount = $mountPair[1];
119
-					$this->logger->debug("Updating mount '{$newMount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $newMount->getMountProvider()]);
120
-					$this->updateCachedMount($newMount);
121
-					/** @psalm-suppress InvalidArgument */
122
-					$this->mountsForUsers[$userUID][$newMount->getKey()] = $newMount;
123
-				}
124
-				$this->connection->commit();
125
-			} catch (\Throwable $e) {
126
-				$this->connection->rollBack();
127
-				throw $e;
128
-			}
129
-
130
-			// Only fire events after all mounts have already been adjusted in the database.
131
-			foreach ($addedMounts as $mount) {
132
-				$this->eventDispatcher->dispatchTyped(new UserMountAddedEvent($mount));
133
-			}
134
-			foreach ($removedMounts as $mount) {
135
-				$this->eventDispatcher->dispatchTyped(new UserMountRemovedEvent($mount));
136
-			}
137
-			foreach ($changedMounts as $mountPair) {
138
-				$this->eventDispatcher->dispatchTyped(new UserMountUpdatedEvent($mountPair[0], $mountPair[1]));
139
-			}
140
-		}
141
-		$this->eventLogger->end('fs:setup:user:register');
142
-	}
143
-
144
-	/**
145
-	 * @param array<string, ICachedMountInfo> $newMounts
146
-	 * @param array<string, ICachedMountInfo> $cachedMounts
147
-	 * @return list<list{0: ICachedMountInfo, 1: ICachedMountInfo}> Pairs of old and new mounts
148
-	 */
149
-	private function findChangedMounts(array $newMounts, array $cachedMounts): array {
150
-		$changed = [];
151
-		foreach ($cachedMounts as $key => $cachedMount) {
152
-			if (isset($newMounts[$key])) {
153
-				$newMount = $newMounts[$key];
154
-				if (
155
-					$newMount->getStorageId() !== $cachedMount->getStorageId()
156
-					|| $newMount->getMountId() !== $cachedMount->getMountId()
157
-					|| $newMount->getMountProvider() !== $cachedMount->getMountProvider()
158
-				) {
159
-					$changed[] = [$cachedMount, $newMount];
160
-				}
161
-			}
162
-		}
163
-		return $changed;
164
-	}
165
-
166
-	private function addToCache(ICachedMountInfo $mount) {
167
-		if ($mount->getStorageId() !== -1) {
168
-			$qb = $this->connection->getQueryBuilder();
169
-			$qb
170
-				->insert('mounts')
171
-				->values([
172
-					'storage_id' => $qb->createNamedParameter($mount->getStorageId(), IQueryBuilder::PARAM_INT),
173
-					'root_id' => $qb->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT),
174
-					'user_id' => $qb->createNamedParameter($mount->getUser()->getUID()),
175
-					'mount_point' => $qb->createNamedParameter($mount->getMountPoint()),
176
-					'mount_point_hash' => $qb->createNamedParameter(hash('xxh128', $mount->getMountPoint())),
177
-					'mount_id' => $qb->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT),
178
-					'mount_provider_class' => $qb->createNamedParameter($mount->getMountProvider()),
179
-				]);
180
-			try {
181
-				$qb->executeStatement();
182
-			} catch (Exception $e) {
183
-				if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
184
-					throw $e;
185
-				}
186
-			}
187
-		} else {
188
-			// in some cases this is legitimate, like orphaned shares
189
-			$this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
190
-		}
191
-	}
192
-
193
-	private function updateCachedMount(ICachedMountInfo $mount) {
194
-		$builder = $this->connection->getQueryBuilder();
195
-
196
-		$query = $builder->update('mounts')
197
-			->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
198
-			->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
199
-			->set('mount_point_hash', $builder->createNamedParameter(hash('xxh128', $mount->getMountPoint())))
200
-			->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
201
-			->set('mount_provider_class', $builder->createNamedParameter($mount->getMountProvider()))
202
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
203
-			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
204
-
205
-		$query->executeStatement();
206
-	}
207
-
208
-	private function removeFromCache(ICachedMountInfo $mount) {
209
-		$builder = $this->connection->getQueryBuilder();
210
-
211
-		$query = $builder->delete('mounts')
212
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
213
-			->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)))
214
-			->andWhere($builder->expr()->eq('mount_point_hash', $builder->createNamedParameter(hash('xxh128', $mount->getMountPoint()))));
215
-		$query->executeStatement();
216
-	}
217
-
218
-	/**
219
-	 * @param array $row
220
-	 * @param (callable(CachedMountInfo): string)|null $pathCallback
221
-	 * @return CachedMountInfo
222
-	 */
223
-	private function dbRowToMountInfo(array $row, ?callable $pathCallback = null): ICachedMountInfo {
224
-		$user = new LazyUser($row['user_id'], $this->userManager);
225
-		$mount_id = $row['mount_id'];
226
-		if (!is_null($mount_id)) {
227
-			$mount_id = (int)$mount_id;
228
-		}
229
-		if ($pathCallback) {
230
-			return new LazyPathCachedMountInfo(
231
-				$user,
232
-				(int)$row['storage_id'],
233
-				(int)$row['root_id'],
234
-				$row['mount_point'],
235
-				$row['mount_provider_class'] ?? '',
236
-				$mount_id,
237
-				$pathCallback,
238
-			);
239
-		} else {
240
-			return new CachedMountInfo(
241
-				$user,
242
-				(int)$row['storage_id'],
243
-				(int)$row['root_id'],
244
-				$row['mount_point'],
245
-				$row['mount_provider_class'] ?? '',
246
-				$mount_id,
247
-				$row['path'] ?? '',
248
-			);
249
-		}
250
-	}
251
-
252
-	/**
253
-	 * @param IUser $user
254
-	 * @return ICachedMountInfo[]
255
-	 */
256
-	public function getMountsForUser(IUser $user) {
257
-		$userUID = $user->getUID();
258
-		if (!$this->userManager->userExists($userUID)) {
259
-			return [];
260
-		}
261
-		if (!isset($this->mountsForUsers[$userUID])) {
262
-			$builder = $this->connection->getQueryBuilder();
263
-			$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'mount_provider_class')
264
-				->from('mounts', 'm')
265
-				->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userUID)));
266
-
267
-			$result = $query->executeQuery();
268
-			$rows = $result->fetchAll();
269
-			$result->closeCursor();
270
-
271
-			/** @var array<string, ICachedMountInfo> $mounts */
272
-			$mounts = [];
273
-			foreach ($rows as $row) {
274
-				$mount = $this->dbRowToMountInfo($row, [$this, 'getInternalPathForMountInfo']);
275
-				if ($mount !== null) {
276
-					$mounts[$mount->getKey()] = $mount;
277
-				}
278
-			}
279
-			$this->mountsForUsers[$userUID] = $mounts;
280
-		}
281
-		return $this->mountsForUsers[$userUID];
282
-	}
283
-
284
-	public function getInternalPathForMountInfo(CachedMountInfo $info): string {
285
-		$cached = $this->internalPathCache->get($info->getRootId());
286
-		if ($cached !== null) {
287
-			return $cached;
288
-		}
289
-		$builder = $this->connection->getQueryBuilder();
290
-		$query = $builder->select('path')
291
-			->from('filecache')
292
-			->where($builder->expr()->eq('fileid', $builder->createNamedParameter($info->getRootId())));
293
-		return $query->executeQuery()->fetchOne() ?: '';
294
-	}
295
-
296
-	/**
297
-	 * @param int $numericStorageId
298
-	 * @param string|null $user limit the results to a single user
299
-	 * @return CachedMountInfo[]
300
-	 */
301
-	public function getMountsForStorageId($numericStorageId, $user = null) {
302
-		$builder = $this->connection->getQueryBuilder();
303
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
304
-			->from('mounts', 'm')
305
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
306
-			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
307
-
308
-		if ($user) {
309
-			$query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter($user)));
310
-		}
311
-
312
-		$result = $query->executeQuery();
313
-		$rows = $result->fetchAll();
314
-		$result->closeCursor();
315
-
316
-		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
317
-	}
318
-
319
-	/**
320
-	 * @param int $rootFileId
321
-	 * @return CachedMountInfo[]
322
-	 */
323
-	public function getMountsForRootId($rootFileId) {
324
-		$builder = $this->connection->getQueryBuilder();
325
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
326
-			->from('mounts', 'm')
327
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
328
-			->where($builder->expr()->eq('root_id', $builder->createNamedParameter($rootFileId, IQueryBuilder::PARAM_INT)));
329
-
330
-		$result = $query->executeQuery();
331
-		$rows = $result->fetchAll();
332
-		$result->closeCursor();
333
-
334
-		return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
335
-	}
336
-
337
-	/**
338
-	 * @param $fileId
339
-	 * @return array{int, string, int}
340
-	 * @throws \OCP\Files\NotFoundException
341
-	 */
342
-	private function getCacheInfoFromFileId($fileId): array {
343
-		if (!isset($this->cacheInfoCache[$fileId])) {
344
-			$builder = $this->connection->getQueryBuilder();
345
-			$query = $builder->select('storage', 'path', 'mimetype')
346
-				->from('filecache')
347
-				->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
348
-
349
-			$result = $query->executeQuery();
350
-			$row = $result->fetch();
351
-			$result->closeCursor();
352
-
353
-			if (is_array($row)) {
354
-				$this->cacheInfoCache[$fileId] = [
355
-					(int)$row['storage'],
356
-					(string)$row['path'],
357
-					(int)$row['mimetype']
358
-				];
359
-			} else {
360
-				throw new NotFoundException('File with id "' . $fileId . '" not found');
361
-			}
362
-		}
363
-		return $this->cacheInfoCache[$fileId];
364
-	}
365
-
366
-	/**
367
-	 * @param int $fileId
368
-	 * @param string|null $user optionally restrict the results to a single user
369
-	 * @return ICachedMountFileInfo[]
370
-	 * @since 9.0.0
371
-	 */
372
-	public function getMountsForFileId($fileId, $user = null) {
373
-		try {
374
-			[$storageId, $internalPath] = $this->getCacheInfoFromFileId($fileId);
375
-		} catch (NotFoundException $e) {
376
-			return [];
377
-		}
378
-
379
-		$builder = $this->connection->getQueryBuilder();
380
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
381
-			->from('mounts', 'm')
382
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
383
-			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
384
-			->andWhere(
385
-				$builder->expr()->orX(
386
-					$builder->expr()->eq('f.fileid', $builder->createNamedParameter($fileId)),
387
-					$builder->expr()->emptyString('f.path'),
388
-					$builder->expr()->eq(
389
-						$builder->func()->concat('f.path', $builder->createNamedParameter('/')),
390
-						$builder->func()->substring(
391
-							$builder->createNamedParameter($internalPath),
392
-							$builder->createNamedParameter(1, IQueryBuilder::PARAM_INT),
393
-							$builder->func()->add(
394
-								$builder->func()->charLength('f.path'),
395
-								$builder->createNamedParameter(1, IQueryBuilder::PARAM_INT),
396
-							),
397
-						),
398
-					),
399
-				)
400
-			);
401
-
402
-		if ($user !== null) {
403
-			$query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter($user)));
404
-		}
405
-		$result = $query->executeQuery();
406
-
407
-		$mounts = [];
408
-		while ($row = $result->fetch()) {
409
-			if ($user === null && !$this->userManager->userExists($row['user_id'])) {
410
-				continue;
411
-			}
412
-
413
-			$mounts[] = new CachedMountFileInfo(
414
-				new LazyUser($row['user_id'], $this->userManager),
415
-				(int)$row['storage_id'],
416
-				(int)$row['root_id'],
417
-				$row['mount_point'],
418
-				$row['mount_id'] === null ? null : (int)$row['mount_id'],
419
-				$row['mount_provider_class'] ?? '',
420
-				$row['path'] ?? '',
421
-				$internalPath,
422
-			);
423
-		}
424
-
425
-		return $mounts;
426
-	}
427
-
428
-	/**
429
-	 * Remove all cached mounts for a user
430
-	 *
431
-	 * @param IUser $user
432
-	 */
433
-	public function removeUserMounts(IUser $user) {
434
-		$builder = $this->connection->getQueryBuilder();
435
-
436
-		$query = $builder->delete('mounts')
437
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
438
-		$query->executeStatement();
439
-	}
440
-
441
-	public function removeUserStorageMount($storageId, $userId) {
442
-		$builder = $this->connection->getQueryBuilder();
443
-
444
-		$query = $builder->delete('mounts')
445
-			->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
446
-			->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
447
-		$query->executeStatement();
448
-	}
449
-
450
-	public function remoteStorageMounts($storageId) {
451
-		$builder = $this->connection->getQueryBuilder();
452
-
453
-		$query = $builder->delete('mounts')
454
-			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
455
-		$query->executeStatement();
456
-	}
457
-
458
-	/**
459
-	 * @param array $users
460
-	 * @return array
461
-	 */
462
-	public function getUsedSpaceForUsers(array $users) {
463
-		$builder = $this->connection->getQueryBuilder();
464
-
465
-		$mountPointHashes = array_map(static fn (IUser $user) => hash('xxh128', '/' . $user->getUID() . '/'), $users);
466
-		$userIds = array_map(static fn (IUser $user) => $user->getUID(), $users);
467
-
468
-		$query = $builder->select('m.user_id', 'f.size')
469
-			->from('mounts', 'm')
470
-			->innerJoin('m', 'filecache', 'f',
471
-				$builder->expr()->andX(
472
-					$builder->expr()->eq('m.storage_id', 'f.storage'),
473
-					$builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files')))
474
-				))
475
-			->where($builder->expr()->in('m.mount_point_hash', $builder->createNamedParameter($mountPointHashes, IQueryBuilder::PARAM_STR_ARRAY)))
476
-			->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
477
-
478
-		$result = $query->executeQuery();
479
-
480
-		$results = [];
481
-		while ($row = $result->fetch()) {
482
-			$results[$row['user_id']] = $row['size'];
483
-		}
484
-		$result->closeCursor();
485
-		return $results;
486
-	}
487
-
488
-	public function clear(): void {
489
-		$this->cacheInfoCache = new CappedMemoryCache();
490
-		$this->mountsForUsers = new CappedMemoryCache();
491
-	}
492
-
493
-	public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
494
-		$mounts = $this->getMountsForUser($user);
495
-		$mountPoints = array_map(function (ICachedMountInfo $mount) {
496
-			return $mount->getMountPoint();
497
-		}, $mounts);
498
-		$mounts = array_combine($mountPoints, $mounts);
499
-
500
-		$current = rtrim($path, '/');
501
-		// walk up the directory tree until we find a path that has a mountpoint set
502
-		// the loop will return if a mountpoint is found or break if none are found
503
-		while (true) {
504
-			$mountPoint = $current . '/';
505
-			if (isset($mounts[$mountPoint])) {
506
-				return $mounts[$mountPoint];
507
-			} elseif ($current === '') {
508
-				break;
509
-			}
510
-
511
-			$current = dirname($current);
512
-			if ($current === '.' || $current === '/') {
513
-				$current = '';
514
-			}
515
-		}
516
-
517
-		throw new NotFoundException('No cached mount for path ' . $path);
518
-	}
519
-
520
-	public function getMountsInPath(IUser $user, string $path): array {
521
-		$path = rtrim($path, '/') . '/';
522
-		$mounts = $this->getMountsForUser($user);
523
-		return array_filter($mounts, function (ICachedMountInfo $mount) use ($path) {
524
-			return $mount->getMountPoint() !== $path && str_starts_with($mount->getMountPoint(), $path);
525
-		});
526
-	}
33
+    /**
34
+     * Cached mount info.
35
+     * @var CappedMemoryCache<ICachedMountInfo[]>
36
+     **/
37
+    private CappedMemoryCache $mountsForUsers;
38
+    /**
39
+     * fileid => internal path mapping for cached mount info.
40
+     * @var CappedMemoryCache<string>
41
+     **/
42
+    private CappedMemoryCache $internalPathCache;
43
+    /** @var CappedMemoryCache<array> */
44
+    private CappedMemoryCache $cacheInfoCache;
45
+
46
+    /**
47
+     * UserMountCache constructor.
48
+     */
49
+    public function __construct(
50
+        private IDBConnection $connection,
51
+        private IUserManager $userManager,
52
+        private LoggerInterface $logger,
53
+        private IEventLogger $eventLogger,
54
+        private IEventDispatcher $eventDispatcher,
55
+    ) {
56
+        $this->cacheInfoCache = new CappedMemoryCache();
57
+        $this->internalPathCache = new CappedMemoryCache();
58
+        $this->mountsForUsers = new CappedMemoryCache();
59
+    }
60
+
61
+    public function registerMounts(IUser $user, array $mounts, ?array $mountProviderClasses = null) {
62
+        $this->eventLogger->start('fs:setup:user:register', 'Registering mounts for user');
63
+        /** @var array<string, ICachedMountInfo> $newMounts */
64
+        $newMounts = [];
65
+        foreach ($mounts as $mount) {
66
+            // filter out any storages which aren't scanned yet since we aren't interested in files from those storages (yet)
67
+            if ($mount->getStorageRootId() !== -1) {
68
+                $mountInfo = new LazyStorageMountInfo($user, $mount);
69
+                $newMounts[$mountInfo->getKey()] = $mountInfo;
70
+            }
71
+        }
72
+
73
+        $cachedMounts = $this->getMountsForUser($user);
74
+        if (is_array($mountProviderClasses)) {
75
+            $cachedMounts = array_filter($cachedMounts, function (ICachedMountInfo $mountInfo) use ($mountProviderClasses, $newMounts) {
76
+                // for existing mounts that didn't have a mount provider set
77
+                // we still want the ones that map to new mounts
78
+                if ($mountInfo->getMountProvider() === '' && isset($newMounts[$mountInfo->getKey()])) {
79
+                    return true;
80
+                }
81
+                return in_array($mountInfo->getMountProvider(), $mountProviderClasses);
82
+            });
83
+        }
84
+
85
+        $addedMounts = [];
86
+        $removedMounts = [];
87
+
88
+        foreach ($newMounts as $mountKey => $newMount) {
89
+            if (!isset($cachedMounts[$mountKey])) {
90
+                $addedMounts[] = $newMount;
91
+            }
92
+        }
93
+
94
+        foreach ($cachedMounts as $mountKey => $cachedMount) {
95
+            if (!isset($newMounts[$mountKey])) {
96
+                $removedMounts[] = $cachedMount;
97
+            }
98
+        }
99
+
100
+        $changedMounts = $this->findChangedMounts($newMounts, $cachedMounts);
101
+
102
+        if ($addedMounts || $removedMounts || $changedMounts) {
103
+            $this->connection->beginTransaction();
104
+            $userUID = $user->getUID();
105
+            try {
106
+                foreach ($addedMounts as $mount) {
107
+                    $this->logger->debug("Adding mount '{$mount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $mount->getMountProvider()]);
108
+                    $this->addToCache($mount);
109
+                    /** @psalm-suppress InvalidArgument */
110
+                    $this->mountsForUsers[$userUID][$mount->getKey()] = $mount;
111
+                }
112
+                foreach ($removedMounts as $mount) {
113
+                    $this->logger->debug("Removing mount '{$mount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $mount->getMountProvider()]);
114
+                    $this->removeFromCache($mount);
115
+                    unset($this->mountsForUsers[$userUID][$mount->getKey()]);
116
+                }
117
+                foreach ($changedMounts as $mountPair) {
118
+                    $newMount = $mountPair[1];
119
+                    $this->logger->debug("Updating mount '{$newMount->getKey()}' for user '$userUID'", ['app' => 'files', 'mount_provider' => $newMount->getMountProvider()]);
120
+                    $this->updateCachedMount($newMount);
121
+                    /** @psalm-suppress InvalidArgument */
122
+                    $this->mountsForUsers[$userUID][$newMount->getKey()] = $newMount;
123
+                }
124
+                $this->connection->commit();
125
+            } catch (\Throwable $e) {
126
+                $this->connection->rollBack();
127
+                throw $e;
128
+            }
129
+
130
+            // Only fire events after all mounts have already been adjusted in the database.
131
+            foreach ($addedMounts as $mount) {
132
+                $this->eventDispatcher->dispatchTyped(new UserMountAddedEvent($mount));
133
+            }
134
+            foreach ($removedMounts as $mount) {
135
+                $this->eventDispatcher->dispatchTyped(new UserMountRemovedEvent($mount));
136
+            }
137
+            foreach ($changedMounts as $mountPair) {
138
+                $this->eventDispatcher->dispatchTyped(new UserMountUpdatedEvent($mountPair[0], $mountPair[1]));
139
+            }
140
+        }
141
+        $this->eventLogger->end('fs:setup:user:register');
142
+    }
143
+
144
+    /**
145
+     * @param array<string, ICachedMountInfo> $newMounts
146
+     * @param array<string, ICachedMountInfo> $cachedMounts
147
+     * @return list<list{0: ICachedMountInfo, 1: ICachedMountInfo}> Pairs of old and new mounts
148
+     */
149
+    private function findChangedMounts(array $newMounts, array $cachedMounts): array {
150
+        $changed = [];
151
+        foreach ($cachedMounts as $key => $cachedMount) {
152
+            if (isset($newMounts[$key])) {
153
+                $newMount = $newMounts[$key];
154
+                if (
155
+                    $newMount->getStorageId() !== $cachedMount->getStorageId()
156
+                    || $newMount->getMountId() !== $cachedMount->getMountId()
157
+                    || $newMount->getMountProvider() !== $cachedMount->getMountProvider()
158
+                ) {
159
+                    $changed[] = [$cachedMount, $newMount];
160
+                }
161
+            }
162
+        }
163
+        return $changed;
164
+    }
165
+
166
+    private function addToCache(ICachedMountInfo $mount) {
167
+        if ($mount->getStorageId() !== -1) {
168
+            $qb = $this->connection->getQueryBuilder();
169
+            $qb
170
+                ->insert('mounts')
171
+                ->values([
172
+                    'storage_id' => $qb->createNamedParameter($mount->getStorageId(), IQueryBuilder::PARAM_INT),
173
+                    'root_id' => $qb->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT),
174
+                    'user_id' => $qb->createNamedParameter($mount->getUser()->getUID()),
175
+                    'mount_point' => $qb->createNamedParameter($mount->getMountPoint()),
176
+                    'mount_point_hash' => $qb->createNamedParameter(hash('xxh128', $mount->getMountPoint())),
177
+                    'mount_id' => $qb->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT),
178
+                    'mount_provider_class' => $qb->createNamedParameter($mount->getMountProvider()),
179
+                ]);
180
+            try {
181
+                $qb->executeStatement();
182
+            } catch (Exception $e) {
183
+                if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
184
+                    throw $e;
185
+                }
186
+            }
187
+        } else {
188
+            // in some cases this is legitimate, like orphaned shares
189
+            $this->logger->debug('Could not get storage info for mount at ' . $mount->getMountPoint());
190
+        }
191
+    }
192
+
193
+    private function updateCachedMount(ICachedMountInfo $mount) {
194
+        $builder = $this->connection->getQueryBuilder();
195
+
196
+        $query = $builder->update('mounts')
197
+            ->set('storage_id', $builder->createNamedParameter($mount->getStorageId()))
198
+            ->set('mount_point', $builder->createNamedParameter($mount->getMountPoint()))
199
+            ->set('mount_point_hash', $builder->createNamedParameter(hash('xxh128', $mount->getMountPoint())))
200
+            ->set('mount_id', $builder->createNamedParameter($mount->getMountId(), IQueryBuilder::PARAM_INT))
201
+            ->set('mount_provider_class', $builder->createNamedParameter($mount->getMountProvider()))
202
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
203
+            ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)));
204
+
205
+        $query->executeStatement();
206
+    }
207
+
208
+    private function removeFromCache(ICachedMountInfo $mount) {
209
+        $builder = $this->connection->getQueryBuilder();
210
+
211
+        $query = $builder->delete('mounts')
212
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($mount->getUser()->getUID())))
213
+            ->andWhere($builder->expr()->eq('root_id', $builder->createNamedParameter($mount->getRootId(), IQueryBuilder::PARAM_INT)))
214
+            ->andWhere($builder->expr()->eq('mount_point_hash', $builder->createNamedParameter(hash('xxh128', $mount->getMountPoint()))));
215
+        $query->executeStatement();
216
+    }
217
+
218
+    /**
219
+     * @param array $row
220
+     * @param (callable(CachedMountInfo): string)|null $pathCallback
221
+     * @return CachedMountInfo
222
+     */
223
+    private function dbRowToMountInfo(array $row, ?callable $pathCallback = null): ICachedMountInfo {
224
+        $user = new LazyUser($row['user_id'], $this->userManager);
225
+        $mount_id = $row['mount_id'];
226
+        if (!is_null($mount_id)) {
227
+            $mount_id = (int)$mount_id;
228
+        }
229
+        if ($pathCallback) {
230
+            return new LazyPathCachedMountInfo(
231
+                $user,
232
+                (int)$row['storage_id'],
233
+                (int)$row['root_id'],
234
+                $row['mount_point'],
235
+                $row['mount_provider_class'] ?? '',
236
+                $mount_id,
237
+                $pathCallback,
238
+            );
239
+        } else {
240
+            return new CachedMountInfo(
241
+                $user,
242
+                (int)$row['storage_id'],
243
+                (int)$row['root_id'],
244
+                $row['mount_point'],
245
+                $row['mount_provider_class'] ?? '',
246
+                $mount_id,
247
+                $row['path'] ?? '',
248
+            );
249
+        }
250
+    }
251
+
252
+    /**
253
+     * @param IUser $user
254
+     * @return ICachedMountInfo[]
255
+     */
256
+    public function getMountsForUser(IUser $user) {
257
+        $userUID = $user->getUID();
258
+        if (!$this->userManager->userExists($userUID)) {
259
+            return [];
260
+        }
261
+        if (!isset($this->mountsForUsers[$userUID])) {
262
+            $builder = $this->connection->getQueryBuilder();
263
+            $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'mount_provider_class')
264
+                ->from('mounts', 'm')
265
+                ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userUID)));
266
+
267
+            $result = $query->executeQuery();
268
+            $rows = $result->fetchAll();
269
+            $result->closeCursor();
270
+
271
+            /** @var array<string, ICachedMountInfo> $mounts */
272
+            $mounts = [];
273
+            foreach ($rows as $row) {
274
+                $mount = $this->dbRowToMountInfo($row, [$this, 'getInternalPathForMountInfo']);
275
+                if ($mount !== null) {
276
+                    $mounts[$mount->getKey()] = $mount;
277
+                }
278
+            }
279
+            $this->mountsForUsers[$userUID] = $mounts;
280
+        }
281
+        return $this->mountsForUsers[$userUID];
282
+    }
283
+
284
+    public function getInternalPathForMountInfo(CachedMountInfo $info): string {
285
+        $cached = $this->internalPathCache->get($info->getRootId());
286
+        if ($cached !== null) {
287
+            return $cached;
288
+        }
289
+        $builder = $this->connection->getQueryBuilder();
290
+        $query = $builder->select('path')
291
+            ->from('filecache')
292
+            ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($info->getRootId())));
293
+        return $query->executeQuery()->fetchOne() ?: '';
294
+    }
295
+
296
+    /**
297
+     * @param int $numericStorageId
298
+     * @param string|null $user limit the results to a single user
299
+     * @return CachedMountInfo[]
300
+     */
301
+    public function getMountsForStorageId($numericStorageId, $user = null) {
302
+        $builder = $this->connection->getQueryBuilder();
303
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
304
+            ->from('mounts', 'm')
305
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
306
+            ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($numericStorageId, IQueryBuilder::PARAM_INT)));
307
+
308
+        if ($user) {
309
+            $query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter($user)));
310
+        }
311
+
312
+        $result = $query->executeQuery();
313
+        $rows = $result->fetchAll();
314
+        $result->closeCursor();
315
+
316
+        return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
317
+    }
318
+
319
+    /**
320
+     * @param int $rootFileId
321
+     * @return CachedMountInfo[]
322
+     */
323
+    public function getMountsForRootId($rootFileId) {
324
+        $builder = $this->connection->getQueryBuilder();
325
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
326
+            ->from('mounts', 'm')
327
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
328
+            ->where($builder->expr()->eq('root_id', $builder->createNamedParameter($rootFileId, IQueryBuilder::PARAM_INT)));
329
+
330
+        $result = $query->executeQuery();
331
+        $rows = $result->fetchAll();
332
+        $result->closeCursor();
333
+
334
+        return array_filter(array_map([$this, 'dbRowToMountInfo'], $rows));
335
+    }
336
+
337
+    /**
338
+     * @param $fileId
339
+     * @return array{int, string, int}
340
+     * @throws \OCP\Files\NotFoundException
341
+     */
342
+    private function getCacheInfoFromFileId($fileId): array {
343
+        if (!isset($this->cacheInfoCache[$fileId])) {
344
+            $builder = $this->connection->getQueryBuilder();
345
+            $query = $builder->select('storage', 'path', 'mimetype')
346
+                ->from('filecache')
347
+                ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
348
+
349
+            $result = $query->executeQuery();
350
+            $row = $result->fetch();
351
+            $result->closeCursor();
352
+
353
+            if (is_array($row)) {
354
+                $this->cacheInfoCache[$fileId] = [
355
+                    (int)$row['storage'],
356
+                    (string)$row['path'],
357
+                    (int)$row['mimetype']
358
+                ];
359
+            } else {
360
+                throw new NotFoundException('File with id "' . $fileId . '" not found');
361
+            }
362
+        }
363
+        return $this->cacheInfoCache[$fileId];
364
+    }
365
+
366
+    /**
367
+     * @param int $fileId
368
+     * @param string|null $user optionally restrict the results to a single user
369
+     * @return ICachedMountFileInfo[]
370
+     * @since 9.0.0
371
+     */
372
+    public function getMountsForFileId($fileId, $user = null) {
373
+        try {
374
+            [$storageId, $internalPath] = $this->getCacheInfoFromFileId($fileId);
375
+        } catch (NotFoundException $e) {
376
+            return [];
377
+        }
378
+
379
+        $builder = $this->connection->getQueryBuilder();
380
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
381
+            ->from('mounts', 'm')
382
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
383
+            ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
384
+            ->andWhere(
385
+                $builder->expr()->orX(
386
+                    $builder->expr()->eq('f.fileid', $builder->createNamedParameter($fileId)),
387
+                    $builder->expr()->emptyString('f.path'),
388
+                    $builder->expr()->eq(
389
+                        $builder->func()->concat('f.path', $builder->createNamedParameter('/')),
390
+                        $builder->func()->substring(
391
+                            $builder->createNamedParameter($internalPath),
392
+                            $builder->createNamedParameter(1, IQueryBuilder::PARAM_INT),
393
+                            $builder->func()->add(
394
+                                $builder->func()->charLength('f.path'),
395
+                                $builder->createNamedParameter(1, IQueryBuilder::PARAM_INT),
396
+                            ),
397
+                        ),
398
+                    ),
399
+                )
400
+            );
401
+
402
+        if ($user !== null) {
403
+            $query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter($user)));
404
+        }
405
+        $result = $query->executeQuery();
406
+
407
+        $mounts = [];
408
+        while ($row = $result->fetch()) {
409
+            if ($user === null && !$this->userManager->userExists($row['user_id'])) {
410
+                continue;
411
+            }
412
+
413
+            $mounts[] = new CachedMountFileInfo(
414
+                new LazyUser($row['user_id'], $this->userManager),
415
+                (int)$row['storage_id'],
416
+                (int)$row['root_id'],
417
+                $row['mount_point'],
418
+                $row['mount_id'] === null ? null : (int)$row['mount_id'],
419
+                $row['mount_provider_class'] ?? '',
420
+                $row['path'] ?? '',
421
+                $internalPath,
422
+            );
423
+        }
424
+
425
+        return $mounts;
426
+    }
427
+
428
+    /**
429
+     * Remove all cached mounts for a user
430
+     *
431
+     * @param IUser $user
432
+     */
433
+    public function removeUserMounts(IUser $user) {
434
+        $builder = $this->connection->getQueryBuilder();
435
+
436
+        $query = $builder->delete('mounts')
437
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())));
438
+        $query->executeStatement();
439
+    }
440
+
441
+    public function removeUserStorageMount($storageId, $userId) {
442
+        $builder = $this->connection->getQueryBuilder();
443
+
444
+        $query = $builder->delete('mounts')
445
+            ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($userId)))
446
+            ->andWhere($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
447
+        $query->executeStatement();
448
+    }
449
+
450
+    public function remoteStorageMounts($storageId) {
451
+        $builder = $this->connection->getQueryBuilder();
452
+
453
+        $query = $builder->delete('mounts')
454
+            ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)));
455
+        $query->executeStatement();
456
+    }
457
+
458
+    /**
459
+     * @param array $users
460
+     * @return array
461
+     */
462
+    public function getUsedSpaceForUsers(array $users) {
463
+        $builder = $this->connection->getQueryBuilder();
464
+
465
+        $mountPointHashes = array_map(static fn (IUser $user) => hash('xxh128', '/' . $user->getUID() . '/'), $users);
466
+        $userIds = array_map(static fn (IUser $user) => $user->getUID(), $users);
467
+
468
+        $query = $builder->select('m.user_id', 'f.size')
469
+            ->from('mounts', 'm')
470
+            ->innerJoin('m', 'filecache', 'f',
471
+                $builder->expr()->andX(
472
+                    $builder->expr()->eq('m.storage_id', 'f.storage'),
473
+                    $builder->expr()->eq('f.path_hash', $builder->createNamedParameter(md5('files')))
474
+                ))
475
+            ->where($builder->expr()->in('m.mount_point_hash', $builder->createNamedParameter($mountPointHashes, IQueryBuilder::PARAM_STR_ARRAY)))
476
+            ->andWhere($builder->expr()->in('m.user_id', $builder->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
477
+
478
+        $result = $query->executeQuery();
479
+
480
+        $results = [];
481
+        while ($row = $result->fetch()) {
482
+            $results[$row['user_id']] = $row['size'];
483
+        }
484
+        $result->closeCursor();
485
+        return $results;
486
+    }
487
+
488
+    public function clear(): void {
489
+        $this->cacheInfoCache = new CappedMemoryCache();
490
+        $this->mountsForUsers = new CappedMemoryCache();
491
+    }
492
+
493
+    public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
494
+        $mounts = $this->getMountsForUser($user);
495
+        $mountPoints = array_map(function (ICachedMountInfo $mount) {
496
+            return $mount->getMountPoint();
497
+        }, $mounts);
498
+        $mounts = array_combine($mountPoints, $mounts);
499
+
500
+        $current = rtrim($path, '/');
501
+        // walk up the directory tree until we find a path that has a mountpoint set
502
+        // the loop will return if a mountpoint is found or break if none are found
503
+        while (true) {
504
+            $mountPoint = $current . '/';
505
+            if (isset($mounts[$mountPoint])) {
506
+                return $mounts[$mountPoint];
507
+            } elseif ($current === '') {
508
+                break;
509
+            }
510
+
511
+            $current = dirname($current);
512
+            if ($current === '.' || $current === '/') {
513
+                $current = '';
514
+            }
515
+        }
516
+
517
+        throw new NotFoundException('No cached mount for path ' . $path);
518
+    }
519
+
520
+    public function getMountsInPath(IUser $user, string $path): array {
521
+        $path = rtrim($path, '/') . '/';
522
+        $mounts = $this->getMountsForUser($user);
523
+        return array_filter($mounts, function (ICachedMountInfo $mount) use ($path) {
524
+            return $mount->getMountPoint() !== $path && str_starts_with($mount->getMountPoint(), $path);
525
+        });
526
+    }
527 527
 }
Please login to merge, or discard this patch.
tests/lib/Files/Cache/FileAccessTest.php 1 patch
Indentation   +429 added lines, -429 removed lines patch added patch discarded remove patch
@@ -21,433 +21,433 @@
 block discarded – undo
21 21
 
22 22
 #[\PHPUnit\Framework\Attributes\Group('DB')]
23 23
 class FileAccessTest extends TestCase {
24
-	private IDBConnection $dbConnection;
25
-	private FileAccess $fileAccess;
26
-
27
-	protected function setUp(): void {
28
-		parent::setUp();
29
-
30
-		// Setup the actual database connection (assume the database is configured properly in PHPUnit setup)
31
-		$this->dbConnection = Server::get(IDBConnection::class);
32
-
33
-		// Ensure FileAccess is instantiated with the real connection
34
-		$this->fileAccess = new FileAccess(
35
-			$this->dbConnection,
36
-			Server::get(SystemConfig::class),
37
-			Server::get(LoggerInterface::class),
38
-			Server::get(FilesMetadataManager::class),
39
-			Server::get(IMimeTypeLoader::class)
40
-		);
41
-
42
-		// Clear and prepare `filecache` table for tests
43
-		$queryBuilder = $this->dbConnection->getQueryBuilder()->runAcrossAllShards();
44
-		$queryBuilder->delete('filecache')->executeStatement();
45
-
46
-		// Clean up potential leftovers from other tests
47
-		$queryBuilder = $this->dbConnection->getQueryBuilder();
48
-		$queryBuilder->delete('mounts')->executeStatement();
49
-
50
-
51
-		$this->setUpTestDatabaseForGetDistinctMounts();
52
-		$this->setUpTestDatabaseForGetByAncestorInStorage();
53
-	}
54
-
55
-	private function setUpTestDatabaseForGetDistinctMounts(): void {
56
-		$queryBuilder = $this->dbConnection->getQueryBuilder();
57
-
58
-		// Insert test data
59
-		$queryBuilder->insert('mounts')
60
-			->values([
61
-				'storage_id' => $queryBuilder->createNamedParameter(1, IQueryBuilder::PARAM_INT),
62
-				'root_id' => $queryBuilder->createNamedParameter(10, IQueryBuilder::PARAM_INT),
63
-				'mount_provider_class' => $queryBuilder->createNamedParameter('TestProviderClass1'),
64
-				'mount_point' => $queryBuilder->createNamedParameter('/files'),
65
-				'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/files')),
66
-				'user_id' => $queryBuilder->createNamedParameter('test'),
67
-			])
68
-			->executeStatement();
69
-
70
-		$queryBuilder->insert('mounts')
71
-			->values([
72
-				'storage_id' => $queryBuilder->createNamedParameter(3, IQueryBuilder::PARAM_INT),
73
-				'root_id' => $queryBuilder->createNamedParameter(30, IQueryBuilder::PARAM_INT),
74
-				'mount_provider_class' => $queryBuilder->createNamedParameter('TestProviderClass1'),
75
-				'mount_point' => $queryBuilder->createNamedParameter('/documents'),
76
-				'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/documents')),
77
-				'user_id' => $queryBuilder->createNamedParameter('test'),
78
-			])
79
-			->executeStatement();
80
-
81
-		$queryBuilder->insert('mounts')
82
-			->values([
83
-				'storage_id' => $queryBuilder->createNamedParameter(4, IQueryBuilder::PARAM_INT),
84
-				'root_id' => $queryBuilder->createNamedParameter(31, IQueryBuilder::PARAM_INT),
85
-				'mount_provider_class' => $queryBuilder->createNamedParameter('TestProviderClass2'),
86
-				'mount_point' => $queryBuilder->createNamedParameter('/foobar'),
87
-				'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/foobar')),
88
-				'user_id' => $queryBuilder->createNamedParameter('test'),
89
-			])
90
-			->executeStatement();
91
-	}
92
-
93
-	/**
94
-	 * Test that getDistinctMounts returns all mounts without filters
95
-	 */
96
-	public function testGetDistinctMountsWithoutFilters(): void {
97
-		$result = iterator_to_array($this->fileAccess->getDistinctMounts([], false));
98
-
99
-		$this->assertCount(3, $result);
100
-
101
-		$this->assertEquals([
102
-			'storage_id' => 1,
103
-			'root_id' => 10,
104
-			'overridden_root' => 10,
105
-		], $result[0]);
106
-
107
-		$this->assertEquals([
108
-			'storage_id' => 3,
109
-			'root_id' => 30,
110
-			'overridden_root' => 30,
111
-		], $result[1]);
112
-
113
-		$this->assertEquals([
114
-			'storage_id' => 4,
115
-			'root_id' => 31,
116
-			'overridden_root' => 31,
117
-		], $result[2]);
118
-	}
119
-
120
-	/**
121
-	 * Test that getDistinctMounts applies filtering by mount providers
122
-	 */
123
-	public function testGetDistinctMountsWithMountProviderFilter(): void {
124
-		$result = iterator_to_array($this->fileAccess->getDistinctMounts(['TestProviderClass1'], false));
125
-
126
-		$this->assertCount(2, $result);
127
-
128
-		$this->assertEquals([
129
-			'storage_id' => 1,
130
-			'root_id' => 10,
131
-			'overridden_root' => 10,
132
-		], $result[0]);
133
-
134
-		$this->assertEquals([
135
-			'storage_id' => 3,
136
-			'root_id' => 30,
137
-			'overridden_root' => 30,
138
-		], $result[1]);
139
-	}
140
-
141
-	/**
142
-	 * Test that getDistinctMounts rewrites home directory paths
143
-	 */
144
-	public function testGetDistinctMountsWithRewriteHomeDirectories(): void {
145
-		// Add additional test data for a home directory mount
146
-		$queryBuilder = $this->dbConnection->getQueryBuilder();
147
-		$queryBuilder->insert('mounts')
148
-			->values([
149
-				'storage_id' => $queryBuilder->createNamedParameter(4, IQueryBuilder::PARAM_INT),
150
-				'root_id' => $queryBuilder->createNamedParameter(40, IQueryBuilder::PARAM_INT),
151
-				'mount_provider_class' => $queryBuilder->createNamedParameter(LocalHomeMountProvider::class),
152
-				'mount_point' => $queryBuilder->createNamedParameter('/home/user'),
153
-				'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/home/user')),
154
-				'user_id' => $queryBuilder->createNamedParameter('test'),
155
-			])
156
-			->executeStatement();
157
-
158
-		// Add a mount that is mounted in the home directory
159
-		$queryBuilder = $this->dbConnection->getQueryBuilder();
160
-		$queryBuilder->insert('mounts')
161
-			->values([
162
-				'storage_id' => $queryBuilder->createNamedParameter(5, IQueryBuilder::PARAM_INT),
163
-				'root_id' => $queryBuilder->createNamedParameter(41, IQueryBuilder::PARAM_INT),
164
-				'mount_provider_class' => $queryBuilder->createNamedParameter('TestMountProvider3'),
165
-				'mount_point' => $queryBuilder->createNamedParameter('/test/files/foobar'),
166
-				'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/test/files/foobar')),
167
-				'user_id' => $queryBuilder->createNamedParameter('test'),
168
-			])
169
-			->executeStatement();
170
-
171
-		// Simulate adding a "files" directory to the filecache table
172
-		$queryBuilder = $this->dbConnection->getQueryBuilder()->runAcrossAllShards();
173
-		$queryBuilder->delete('filecache')->executeStatement();
174
-		$queryBuilder = $this->dbConnection->getQueryBuilder();
175
-		$queryBuilder->insert('filecache')
176
-			->values([
177
-				'fileid' => $queryBuilder->createNamedParameter(99, IQueryBuilder::PARAM_INT),
178
-				'storage' => $queryBuilder->createNamedParameter(4, IQueryBuilder::PARAM_INT),
179
-				'parent' => $queryBuilder->createNamedParameter(40),
180
-				'name' => $queryBuilder->createNamedParameter('files'),
181
-				'path' => $queryBuilder->createNamedParameter('files'),
182
-				'path_hash' => $queryBuilder->createNamedParameter(md5('files')),
183
-			])
184
-			->executeStatement();
185
-
186
-		$result = iterator_to_array($this->fileAccess->getDistinctMounts());
187
-
188
-		$this->assertCount(2, $result);
189
-
190
-		$this->assertEquals([
191
-			'storage_id' => 4,
192
-			'root_id' => 40,
193
-			'overridden_root' => 99,
194
-		], $result[0]);
195
-
196
-		$this->assertEquals([
197
-			'storage_id' => 5,
198
-			'root_id' => 41,
199
-			'overridden_root' => 41,
200
-		], $result[1]);
201
-	}
202
-
203
-	private function setUpTestDatabaseForGetByAncestorInStorage(): void {
204
-		// prepare `filecache` table for tests
205
-		$queryBuilder = $this->dbConnection->getQueryBuilder();
206
-
207
-		$queryBuilder->insert('filecache')
208
-			->values([
209
-				'fileid' => 1,
210
-				'parent' => 0,
211
-				'path' => $queryBuilder->createNamedParameter('files'),
212
-				'path_hash' => $queryBuilder->createNamedParameter(md5('files')),
213
-				'storage' => $queryBuilder->createNamedParameter(1),
214
-				'name' => $queryBuilder->createNamedParameter('files'),
215
-				'mimetype' => 1,
216
-				'encrypted' => 0,
217
-				'size' => 1,
218
-			])
219
-			->executeStatement();
220
-
221
-		$queryBuilder->insert('filecache')
222
-			->values([
223
-				'fileid' => 2,
224
-				'parent' => 1,
225
-				'path' => $queryBuilder->createNamedParameter('files/documents'),
226
-				'path_hash' => $queryBuilder->createNamedParameter(md5('files/documents')),
227
-				'storage' => $queryBuilder->createNamedParameter(1),
228
-				'name' => $queryBuilder->createNamedParameter('documents'),
229
-				'mimetype' => 2,
230
-				'encrypted' => 1,
231
-				'size' => 1,
232
-			])
233
-			->executeStatement();
234
-
235
-		$queryBuilder->insert('filecache')
236
-			->values([
237
-				'fileid' => 3,
238
-				'parent' => 1,
239
-				'path' => $queryBuilder->createNamedParameter('files/photos'),
240
-				'path_hash' => $queryBuilder->createNamedParameter(md5('files/photos')),
241
-				'storage' => $queryBuilder->createNamedParameter(1),
242
-				'name' => $queryBuilder->createNamedParameter('photos'),
243
-				'mimetype' => 3,
244
-				'encrypted' => 1,
245
-				'size' => 1,
246
-			])
247
-			->executeStatement();
248
-
249
-		$queryBuilder->insert('filecache')
250
-			->values([
251
-				'fileid' => 4,
252
-				'parent' => 3,
253
-				'path' => $queryBuilder->createNamedParameter('files/photos/endtoendencrypted'),
254
-				'path_hash' => $queryBuilder->createNamedParameter(md5('files/photos/endtoendencrypted')),
255
-				'storage' => $queryBuilder->createNamedParameter(1),
256
-				'name' => $queryBuilder->createNamedParameter('endtoendencrypted'),
257
-				'mimetype' => 4,
258
-				'encrypted' => 0,
259
-				'size' => 1,
260
-			])
261
-			->executeStatement();
262
-
263
-		$queryBuilder->insert('filecache')
264
-			->values([
265
-				'fileid' => 5,
266
-				'parent' => 1,
267
-				'path' => $queryBuilder->createNamedParameter('files/serversideencrypted'),
268
-				'path_hash' => $queryBuilder->createNamedParameter(md5('files/serversideencrypted')),
269
-				'storage' => $queryBuilder->createNamedParameter(1),
270
-				'name' => $queryBuilder->createNamedParameter('serversideencrypted'),
271
-				'mimetype' => 4,
272
-				'encrypted' => 1,
273
-				'size' => 1,
274
-			])
275
-			->executeStatement();
276
-
277
-		$queryBuilder->insert('filecache')
278
-			->values([
279
-				'fileid' => 6,
280
-				'parent' => 0,
281
-				'path' => $queryBuilder->createNamedParameter('files/storage2'),
282
-				'path_hash' => $queryBuilder->createNamedParameter(md5('files/storage2')),
283
-				'storage' => $queryBuilder->createNamedParameter(2),
284
-				'name' => $queryBuilder->createNamedParameter('storage2'),
285
-				'mimetype' => 5,
286
-				'encrypted' => 0,
287
-				'size' => 1,
288
-			])
289
-			->executeStatement();
290
-
291
-		$queryBuilder->insert('filecache')
292
-			->values([
293
-				'fileid' => 7,
294
-				'parent' => 6,
295
-				'path' => $queryBuilder->createNamedParameter('files/storage2/file'),
296
-				'path_hash' => $queryBuilder->createNamedParameter(md5('files/storage2/file')),
297
-				'storage' => $queryBuilder->createNamedParameter(2),
298
-				'name' => $queryBuilder->createNamedParameter('file'),
299
-				'mimetype' => 6,
300
-				'encrypted' => 0,
301
-				'size' => 1,
302
-			])
303
-			->executeStatement();
304
-	}
305
-
306
-	/**
307
-	 * Test fetching files by ancestor in storage.
308
-	 */
309
-	public function testGetByAncestorInStorage(): void {
310
-		$generator = $this->fileAccess->getByAncestorInStorage(
311
-			1, // storageId
312
-			1, // rootId
313
-			0, // lastFileId
314
-			10, // maxResults
315
-			[], // mimeTypes
316
-			true, // include end-to-end encrypted files
317
-			true, // include server-side encrypted files
318
-		);
319
-
320
-		$result = iterator_to_array($generator);
321
-
322
-		$this->assertCount(4, $result);
323
-
324
-		$paths = array_map(fn (CacheEntry $entry) => $entry->getPath(), $result);
325
-		$this->assertEquals([
326
-			'files/documents',
327
-			'files/photos',
328
-			'files/photos/endtoendencrypted',
329
-			'files/serversideencrypted',
330
-		], $paths);
331
-	}
332
-
333
-	/**
334
-	 * Test filtering by mime types.
335
-	 */
336
-	public function testGetByAncestorInStorageWithMimeTypes(): void {
337
-		$generator = $this->fileAccess->getByAncestorInStorage(
338
-			1,
339
-			1,
340
-			0,
341
-			10,
342
-			[2], // Only include documents (mimetype=2)
343
-			true,
344
-			true,
345
-		);
346
-
347
-		$result = iterator_to_array($generator);
348
-
349
-		$this->assertCount(1, $result);
350
-		$this->assertEquals('files/documents', $result[0]->getPath());
351
-	}
352
-
353
-	/**
354
-	 * Test excluding end-to-end encrypted files.
355
-	 */
356
-	public function testGetByAncestorInStorageWithoutEndToEndEncrypted(): void {
357
-		$generator = $this->fileAccess->getByAncestorInStorage(
358
-			1,
359
-			1,
360
-			0,
361
-			10,
362
-			[],
363
-			false, // exclude end-to-end encrypted files
364
-			true,
365
-		);
366
-
367
-		$result = iterator_to_array($generator);
368
-
369
-		$this->assertCount(3, $result);
370
-		$paths = array_map(fn (CacheEntry $entry) => $entry->getPath(), $result);
371
-		$this->assertEquals(['files/documents', 'files/photos', 'files/serversideencrypted'], $paths);
372
-	}
373
-
374
-	/**
375
-	 * Test excluding server-side encrypted files.
376
-	 */
377
-	public function testGetByAncestorInStorageWithoutServerSideEncrypted(): void {
378
-		$generator = $this->fileAccess->getByAncestorInStorage(
379
-			1,
380
-			1,
381
-			0,
382
-			10,
383
-			[],
384
-			true,
385
-			false, // exclude server-side encrypted files
386
-		);
387
-
388
-		$result = iterator_to_array($generator);
389
-
390
-		$this->assertCount(1, $result);
391
-		$this->assertEquals('files/photos/endtoendencrypted', $result[0]->getPath());
392
-	}
393
-
394
-	/**
395
-	 * Test max result limits.
396
-	 */
397
-	public function testGetByAncestorInStorageWithMaxResults(): void {
398
-		$generator = $this->fileAccess->getByAncestorInStorage(
399
-			1,
400
-			1,
401
-			0,
402
-			1, // Limit to 1 result
403
-			[],
404
-			true,
405
-			true,
406
-		);
407
-
408
-		$result = iterator_to_array($generator);
409
-
410
-		$this->assertCount(1, $result);
411
-		$this->assertEquals('files/documents', $result[0]->getPath());
412
-	}
413
-
414
-	/**
415
-	 * Test rootId filter
416
-	 */
417
-	public function testGetByAncestorInStorageWithRootIdFilter(): void {
418
-		$generator = $this->fileAccess->getByAncestorInStorage(
419
-			1,
420
-			3, // Filter by rootId
421
-			0,
422
-			10,
423
-			[],
424
-			true,
425
-			true,
426
-		);
427
-
428
-		$result = iterator_to_array($generator);
429
-
430
-		$this->assertCount(1, $result);
431
-		$this->assertEquals('files/photos/endtoendencrypted', $result[0]->getPath());
432
-	}
433
-
434
-	/**
435
-	 * Test rootId filter
436
-	 */
437
-	public function testGetByAncestorInStorageWithStorageFilter(): void {
438
-		$generator = $this->fileAccess->getByAncestorInStorage(
439
-			2, // Filter by storage
440
-			6, // and by rootId
441
-			0,
442
-			10,
443
-			[],
444
-			true,
445
-			true,
446
-		);
447
-
448
-		$result = iterator_to_array($generator);
449
-
450
-		$this->assertCount(1, $result);
451
-		$this->assertEquals('files/storage2/file', $result[0]->getPath());
452
-	}
24
+    private IDBConnection $dbConnection;
25
+    private FileAccess $fileAccess;
26
+
27
+    protected function setUp(): void {
28
+        parent::setUp();
29
+
30
+        // Setup the actual database connection (assume the database is configured properly in PHPUnit setup)
31
+        $this->dbConnection = Server::get(IDBConnection::class);
32
+
33
+        // Ensure FileAccess is instantiated with the real connection
34
+        $this->fileAccess = new FileAccess(
35
+            $this->dbConnection,
36
+            Server::get(SystemConfig::class),
37
+            Server::get(LoggerInterface::class),
38
+            Server::get(FilesMetadataManager::class),
39
+            Server::get(IMimeTypeLoader::class)
40
+        );
41
+
42
+        // Clear and prepare `filecache` table for tests
43
+        $queryBuilder = $this->dbConnection->getQueryBuilder()->runAcrossAllShards();
44
+        $queryBuilder->delete('filecache')->executeStatement();
45
+
46
+        // Clean up potential leftovers from other tests
47
+        $queryBuilder = $this->dbConnection->getQueryBuilder();
48
+        $queryBuilder->delete('mounts')->executeStatement();
49
+
50
+
51
+        $this->setUpTestDatabaseForGetDistinctMounts();
52
+        $this->setUpTestDatabaseForGetByAncestorInStorage();
53
+    }
54
+
55
+    private function setUpTestDatabaseForGetDistinctMounts(): void {
56
+        $queryBuilder = $this->dbConnection->getQueryBuilder();
57
+
58
+        // Insert test data
59
+        $queryBuilder->insert('mounts')
60
+            ->values([
61
+                'storage_id' => $queryBuilder->createNamedParameter(1, IQueryBuilder::PARAM_INT),
62
+                'root_id' => $queryBuilder->createNamedParameter(10, IQueryBuilder::PARAM_INT),
63
+                'mount_provider_class' => $queryBuilder->createNamedParameter('TestProviderClass1'),
64
+                'mount_point' => $queryBuilder->createNamedParameter('/files'),
65
+                'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/files')),
66
+                'user_id' => $queryBuilder->createNamedParameter('test'),
67
+            ])
68
+            ->executeStatement();
69
+
70
+        $queryBuilder->insert('mounts')
71
+            ->values([
72
+                'storage_id' => $queryBuilder->createNamedParameter(3, IQueryBuilder::PARAM_INT),
73
+                'root_id' => $queryBuilder->createNamedParameter(30, IQueryBuilder::PARAM_INT),
74
+                'mount_provider_class' => $queryBuilder->createNamedParameter('TestProviderClass1'),
75
+                'mount_point' => $queryBuilder->createNamedParameter('/documents'),
76
+                'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/documents')),
77
+                'user_id' => $queryBuilder->createNamedParameter('test'),
78
+            ])
79
+            ->executeStatement();
80
+
81
+        $queryBuilder->insert('mounts')
82
+            ->values([
83
+                'storage_id' => $queryBuilder->createNamedParameter(4, IQueryBuilder::PARAM_INT),
84
+                'root_id' => $queryBuilder->createNamedParameter(31, IQueryBuilder::PARAM_INT),
85
+                'mount_provider_class' => $queryBuilder->createNamedParameter('TestProviderClass2'),
86
+                'mount_point' => $queryBuilder->createNamedParameter('/foobar'),
87
+                'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/foobar')),
88
+                'user_id' => $queryBuilder->createNamedParameter('test'),
89
+            ])
90
+            ->executeStatement();
91
+    }
92
+
93
+    /**
94
+     * Test that getDistinctMounts returns all mounts without filters
95
+     */
96
+    public function testGetDistinctMountsWithoutFilters(): void {
97
+        $result = iterator_to_array($this->fileAccess->getDistinctMounts([], false));
98
+
99
+        $this->assertCount(3, $result);
100
+
101
+        $this->assertEquals([
102
+            'storage_id' => 1,
103
+            'root_id' => 10,
104
+            'overridden_root' => 10,
105
+        ], $result[0]);
106
+
107
+        $this->assertEquals([
108
+            'storage_id' => 3,
109
+            'root_id' => 30,
110
+            'overridden_root' => 30,
111
+        ], $result[1]);
112
+
113
+        $this->assertEquals([
114
+            'storage_id' => 4,
115
+            'root_id' => 31,
116
+            'overridden_root' => 31,
117
+        ], $result[2]);
118
+    }
119
+
120
+    /**
121
+     * Test that getDistinctMounts applies filtering by mount providers
122
+     */
123
+    public function testGetDistinctMountsWithMountProviderFilter(): void {
124
+        $result = iterator_to_array($this->fileAccess->getDistinctMounts(['TestProviderClass1'], false));
125
+
126
+        $this->assertCount(2, $result);
127
+
128
+        $this->assertEquals([
129
+            'storage_id' => 1,
130
+            'root_id' => 10,
131
+            'overridden_root' => 10,
132
+        ], $result[0]);
133
+
134
+        $this->assertEquals([
135
+            'storage_id' => 3,
136
+            'root_id' => 30,
137
+            'overridden_root' => 30,
138
+        ], $result[1]);
139
+    }
140
+
141
+    /**
142
+     * Test that getDistinctMounts rewrites home directory paths
143
+     */
144
+    public function testGetDistinctMountsWithRewriteHomeDirectories(): void {
145
+        // Add additional test data for a home directory mount
146
+        $queryBuilder = $this->dbConnection->getQueryBuilder();
147
+        $queryBuilder->insert('mounts')
148
+            ->values([
149
+                'storage_id' => $queryBuilder->createNamedParameter(4, IQueryBuilder::PARAM_INT),
150
+                'root_id' => $queryBuilder->createNamedParameter(40, IQueryBuilder::PARAM_INT),
151
+                'mount_provider_class' => $queryBuilder->createNamedParameter(LocalHomeMountProvider::class),
152
+                'mount_point' => $queryBuilder->createNamedParameter('/home/user'),
153
+                'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/home/user')),
154
+                'user_id' => $queryBuilder->createNamedParameter('test'),
155
+            ])
156
+            ->executeStatement();
157
+
158
+        // Add a mount that is mounted in the home directory
159
+        $queryBuilder = $this->dbConnection->getQueryBuilder();
160
+        $queryBuilder->insert('mounts')
161
+            ->values([
162
+                'storage_id' => $queryBuilder->createNamedParameter(5, IQueryBuilder::PARAM_INT),
163
+                'root_id' => $queryBuilder->createNamedParameter(41, IQueryBuilder::PARAM_INT),
164
+                'mount_provider_class' => $queryBuilder->createNamedParameter('TestMountProvider3'),
165
+                'mount_point' => $queryBuilder->createNamedParameter('/test/files/foobar'),
166
+                'mount_point_hash' => $queryBuilder->createNamedParameter(hash('xxh128', '/test/files/foobar')),
167
+                'user_id' => $queryBuilder->createNamedParameter('test'),
168
+            ])
169
+            ->executeStatement();
170
+
171
+        // Simulate adding a "files" directory to the filecache table
172
+        $queryBuilder = $this->dbConnection->getQueryBuilder()->runAcrossAllShards();
173
+        $queryBuilder->delete('filecache')->executeStatement();
174
+        $queryBuilder = $this->dbConnection->getQueryBuilder();
175
+        $queryBuilder->insert('filecache')
176
+            ->values([
177
+                'fileid' => $queryBuilder->createNamedParameter(99, IQueryBuilder::PARAM_INT),
178
+                'storage' => $queryBuilder->createNamedParameter(4, IQueryBuilder::PARAM_INT),
179
+                'parent' => $queryBuilder->createNamedParameter(40),
180
+                'name' => $queryBuilder->createNamedParameter('files'),
181
+                'path' => $queryBuilder->createNamedParameter('files'),
182
+                'path_hash' => $queryBuilder->createNamedParameter(md5('files')),
183
+            ])
184
+            ->executeStatement();
185
+
186
+        $result = iterator_to_array($this->fileAccess->getDistinctMounts());
187
+
188
+        $this->assertCount(2, $result);
189
+
190
+        $this->assertEquals([
191
+            'storage_id' => 4,
192
+            'root_id' => 40,
193
+            'overridden_root' => 99,
194
+        ], $result[0]);
195
+
196
+        $this->assertEquals([
197
+            'storage_id' => 5,
198
+            'root_id' => 41,
199
+            'overridden_root' => 41,
200
+        ], $result[1]);
201
+    }
202
+
203
+    private function setUpTestDatabaseForGetByAncestorInStorage(): void {
204
+        // prepare `filecache` table for tests
205
+        $queryBuilder = $this->dbConnection->getQueryBuilder();
206
+
207
+        $queryBuilder->insert('filecache')
208
+            ->values([
209
+                'fileid' => 1,
210
+                'parent' => 0,
211
+                'path' => $queryBuilder->createNamedParameter('files'),
212
+                'path_hash' => $queryBuilder->createNamedParameter(md5('files')),
213
+                'storage' => $queryBuilder->createNamedParameter(1),
214
+                'name' => $queryBuilder->createNamedParameter('files'),
215
+                'mimetype' => 1,
216
+                'encrypted' => 0,
217
+                'size' => 1,
218
+            ])
219
+            ->executeStatement();
220
+
221
+        $queryBuilder->insert('filecache')
222
+            ->values([
223
+                'fileid' => 2,
224
+                'parent' => 1,
225
+                'path' => $queryBuilder->createNamedParameter('files/documents'),
226
+                'path_hash' => $queryBuilder->createNamedParameter(md5('files/documents')),
227
+                'storage' => $queryBuilder->createNamedParameter(1),
228
+                'name' => $queryBuilder->createNamedParameter('documents'),
229
+                'mimetype' => 2,
230
+                'encrypted' => 1,
231
+                'size' => 1,
232
+            ])
233
+            ->executeStatement();
234
+
235
+        $queryBuilder->insert('filecache')
236
+            ->values([
237
+                'fileid' => 3,
238
+                'parent' => 1,
239
+                'path' => $queryBuilder->createNamedParameter('files/photos'),
240
+                'path_hash' => $queryBuilder->createNamedParameter(md5('files/photos')),
241
+                'storage' => $queryBuilder->createNamedParameter(1),
242
+                'name' => $queryBuilder->createNamedParameter('photos'),
243
+                'mimetype' => 3,
244
+                'encrypted' => 1,
245
+                'size' => 1,
246
+            ])
247
+            ->executeStatement();
248
+
249
+        $queryBuilder->insert('filecache')
250
+            ->values([
251
+                'fileid' => 4,
252
+                'parent' => 3,
253
+                'path' => $queryBuilder->createNamedParameter('files/photos/endtoendencrypted'),
254
+                'path_hash' => $queryBuilder->createNamedParameter(md5('files/photos/endtoendencrypted')),
255
+                'storage' => $queryBuilder->createNamedParameter(1),
256
+                'name' => $queryBuilder->createNamedParameter('endtoendencrypted'),
257
+                'mimetype' => 4,
258
+                'encrypted' => 0,
259
+                'size' => 1,
260
+            ])
261
+            ->executeStatement();
262
+
263
+        $queryBuilder->insert('filecache')
264
+            ->values([
265
+                'fileid' => 5,
266
+                'parent' => 1,
267
+                'path' => $queryBuilder->createNamedParameter('files/serversideencrypted'),
268
+                'path_hash' => $queryBuilder->createNamedParameter(md5('files/serversideencrypted')),
269
+                'storage' => $queryBuilder->createNamedParameter(1),
270
+                'name' => $queryBuilder->createNamedParameter('serversideencrypted'),
271
+                'mimetype' => 4,
272
+                'encrypted' => 1,
273
+                'size' => 1,
274
+            ])
275
+            ->executeStatement();
276
+
277
+        $queryBuilder->insert('filecache')
278
+            ->values([
279
+                'fileid' => 6,
280
+                'parent' => 0,
281
+                'path' => $queryBuilder->createNamedParameter('files/storage2'),
282
+                'path_hash' => $queryBuilder->createNamedParameter(md5('files/storage2')),
283
+                'storage' => $queryBuilder->createNamedParameter(2),
284
+                'name' => $queryBuilder->createNamedParameter('storage2'),
285
+                'mimetype' => 5,
286
+                'encrypted' => 0,
287
+                'size' => 1,
288
+            ])
289
+            ->executeStatement();
290
+
291
+        $queryBuilder->insert('filecache')
292
+            ->values([
293
+                'fileid' => 7,
294
+                'parent' => 6,
295
+                'path' => $queryBuilder->createNamedParameter('files/storage2/file'),
296
+                'path_hash' => $queryBuilder->createNamedParameter(md5('files/storage2/file')),
297
+                'storage' => $queryBuilder->createNamedParameter(2),
298
+                'name' => $queryBuilder->createNamedParameter('file'),
299
+                'mimetype' => 6,
300
+                'encrypted' => 0,
301
+                'size' => 1,
302
+            ])
303
+            ->executeStatement();
304
+    }
305
+
306
+    /**
307
+     * Test fetching files by ancestor in storage.
308
+     */
309
+    public function testGetByAncestorInStorage(): void {
310
+        $generator = $this->fileAccess->getByAncestorInStorage(
311
+            1, // storageId
312
+            1, // rootId
313
+            0, // lastFileId
314
+            10, // maxResults
315
+            [], // mimeTypes
316
+            true, // include end-to-end encrypted files
317
+            true, // include server-side encrypted files
318
+        );
319
+
320
+        $result = iterator_to_array($generator);
321
+
322
+        $this->assertCount(4, $result);
323
+
324
+        $paths = array_map(fn (CacheEntry $entry) => $entry->getPath(), $result);
325
+        $this->assertEquals([
326
+            'files/documents',
327
+            'files/photos',
328
+            'files/photos/endtoendencrypted',
329
+            'files/serversideencrypted',
330
+        ], $paths);
331
+    }
332
+
333
+    /**
334
+     * Test filtering by mime types.
335
+     */
336
+    public function testGetByAncestorInStorageWithMimeTypes(): void {
337
+        $generator = $this->fileAccess->getByAncestorInStorage(
338
+            1,
339
+            1,
340
+            0,
341
+            10,
342
+            [2], // Only include documents (mimetype=2)
343
+            true,
344
+            true,
345
+        );
346
+
347
+        $result = iterator_to_array($generator);
348
+
349
+        $this->assertCount(1, $result);
350
+        $this->assertEquals('files/documents', $result[0]->getPath());
351
+    }
352
+
353
+    /**
354
+     * Test excluding end-to-end encrypted files.
355
+     */
356
+    public function testGetByAncestorInStorageWithoutEndToEndEncrypted(): void {
357
+        $generator = $this->fileAccess->getByAncestorInStorage(
358
+            1,
359
+            1,
360
+            0,
361
+            10,
362
+            [],
363
+            false, // exclude end-to-end encrypted files
364
+            true,
365
+        );
366
+
367
+        $result = iterator_to_array($generator);
368
+
369
+        $this->assertCount(3, $result);
370
+        $paths = array_map(fn (CacheEntry $entry) => $entry->getPath(), $result);
371
+        $this->assertEquals(['files/documents', 'files/photos', 'files/serversideencrypted'], $paths);
372
+    }
373
+
374
+    /**
375
+     * Test excluding server-side encrypted files.
376
+     */
377
+    public function testGetByAncestorInStorageWithoutServerSideEncrypted(): void {
378
+        $generator = $this->fileAccess->getByAncestorInStorage(
379
+            1,
380
+            1,
381
+            0,
382
+            10,
383
+            [],
384
+            true,
385
+            false, // exclude server-side encrypted files
386
+        );
387
+
388
+        $result = iterator_to_array($generator);
389
+
390
+        $this->assertCount(1, $result);
391
+        $this->assertEquals('files/photos/endtoendencrypted', $result[0]->getPath());
392
+    }
393
+
394
+    /**
395
+     * Test max result limits.
396
+     */
397
+    public function testGetByAncestorInStorageWithMaxResults(): void {
398
+        $generator = $this->fileAccess->getByAncestorInStorage(
399
+            1,
400
+            1,
401
+            0,
402
+            1, // Limit to 1 result
403
+            [],
404
+            true,
405
+            true,
406
+        );
407
+
408
+        $result = iterator_to_array($generator);
409
+
410
+        $this->assertCount(1, $result);
411
+        $this->assertEquals('files/documents', $result[0]->getPath());
412
+    }
413
+
414
+    /**
415
+     * Test rootId filter
416
+     */
417
+    public function testGetByAncestorInStorageWithRootIdFilter(): void {
418
+        $generator = $this->fileAccess->getByAncestorInStorage(
419
+            1,
420
+            3, // Filter by rootId
421
+            0,
422
+            10,
423
+            [],
424
+            true,
425
+            true,
426
+        );
427
+
428
+        $result = iterator_to_array($generator);
429
+
430
+        $this->assertCount(1, $result);
431
+        $this->assertEquals('files/photos/endtoendencrypted', $result[0]->getPath());
432
+    }
433
+
434
+    /**
435
+     * Test rootId filter
436
+     */
437
+    public function testGetByAncestorInStorageWithStorageFilter(): void {
438
+        $generator = $this->fileAccess->getByAncestorInStorage(
439
+            2, // Filter by storage
440
+            6, // and by rootId
441
+            0,
442
+            10,
443
+            [],
444
+            true,
445
+            true,
446
+        );
447
+
448
+        $result = iterator_to_array($generator);
449
+
450
+        $this->assertCount(1, $result);
451
+        $this->assertEquals('files/storage2/file', $result[0]->getPath());
452
+    }
453 453
 }
Please login to merge, or discard this patch.
tests/lib/DB/QueryBuilder/Partitioned/PartitionedQueryBuilderTest.php 1 patch
Indentation   +208 added lines, -208 removed lines patch added patch discarded remove patch
@@ -20,212 +20,212 @@
 block discarded – undo
20 20
 
21 21
 #[\PHPUnit\Framework\Attributes\Group('DB')]
22 22
 class PartitionedQueryBuilderTest extends TestCase {
23
-	private IDBConnection $connection;
24
-	private ShardConnectionManager $shardConnectionManager;
25
-	private AutoIncrementHandler $autoIncrementHandler;
26
-
27
-	protected function setUp(): void {
28
-		if (PHP_INT_SIZE < 8) {
29
-			$this->markTestSkipped('Test requires 64bit');
30
-			return;
31
-		}
32
-		$this->connection = Server::get(IDBConnection::class);
33
-		$this->shardConnectionManager = Server::get(ShardConnectionManager::class);
34
-		$this->autoIncrementHandler = Server::get(AutoIncrementHandler::class);
35
-
36
-		$this->setupFileCache();
37
-	}
38
-
39
-	protected function tearDown(): void {
40
-		// PHP unit also runs tearDown when the test is skipped, but we only initialized when using 64bit
41
-		// see https://github.com/sebastianbergmann/phpunit/issues/6394
42
-		if (PHP_INT_SIZE >= 8) {
43
-			$this->cleanupDb();
44
-		}
45
-		parent::tearDown();
46
-	}
47
-
48
-
49
-	private function getQueryBuilder(): PartitionedQueryBuilder {
50
-		$builder = $this->connection->getQueryBuilder();
51
-		if ($builder instanceof PartitionedQueryBuilder) {
52
-			return $builder;
53
-		} else {
54
-			return new PartitionedQueryBuilder($builder, [], $this->shardConnectionManager, $this->autoIncrementHandler);
55
-		}
56
-	}
57
-
58
-	private function setupFileCache(): void {
59
-		$this->cleanupDb();
60
-		$query = $this->getQueryBuilder();
61
-		$query->insert('storages')
62
-			->values([
63
-				'numeric_id' => $query->createNamedParameter(1001001, IQueryBuilder::PARAM_INT),
64
-				'id' => $query->createNamedParameter('test1'),
65
-			]);
66
-		$query->executeStatement();
67
-
68
-		$query = $this->getQueryBuilder();
69
-		$query->insert('filecache')
70
-			->values([
71
-				'storage' => $query->createNamedParameter(1001001, IQueryBuilder::PARAM_INT),
72
-				'path' => $query->createNamedParameter('file1'),
73
-				'path_hash' => $query->createNamedParameter(md5('file1')),
74
-			]);
75
-		$query->executeStatement();
76
-		$fileId = $query->getLastInsertId();
77
-
78
-		$query = $this->getQueryBuilder();
79
-		$query->insert('filecache_extended')
80
-			->hintShardKey('storage', 1001001)
81
-			->values([
82
-				'fileid' => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT),
83
-				'upload_time' => $query->createNamedParameter(1234, IQueryBuilder::PARAM_INT),
84
-			]);
85
-		$query->executeStatement();
86
-
87
-		$query = $this->getQueryBuilder();
88
-		$query->insert('mounts')
89
-			->values([
90
-				'storage_id' => $query->createNamedParameter(1001001, IQueryBuilder::PARAM_INT),
91
-				'user_id' => $query->createNamedParameter('partitioned_test'),
92
-				'mount_point' => $query->createNamedParameter('/mount/point'),
93
-				'mount_point_hash' => $query->createNamedParameter(hash('xxh128', '/mount/point')),
94
-				'mount_provider_class' => $query->createNamedParameter('test'),
95
-				'root_id' => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT),
96
-			]);
97
-		$query->executeStatement();
98
-	}
99
-
100
-	private function cleanupDb(): void {
101
-		$query = $this->getQueryBuilder();
102
-		$query->delete('storages')
103
-			->where($query->expr()->gt('numeric_id', $query->createNamedParameter(1000000, IQueryBuilder::PARAM_INT)));
104
-		$query->executeStatement();
105
-
106
-		$query = $this->getQueryBuilder();
107
-		$query->delete('filecache')
108
-			->where($query->expr()->gt('storage', $query->createNamedParameter(1000000, IQueryBuilder::PARAM_INT)))
109
-			->runAcrossAllShards();
110
-		$query->executeStatement();
111
-
112
-		$query = $this->getQueryBuilder();
113
-		$query->delete('filecache_extended')
114
-			->runAcrossAllShards();
115
-		$query->executeStatement();
116
-
117
-		$query = $this->getQueryBuilder();
118
-		$query->delete('mounts')
119
-			->where($query->expr()->like('user_id', $query->createNamedParameter('partitioned_%')));
120
-		$query->executeStatement();
121
-	}
122
-
123
-	public function testSimpleOnlyPartitionQuery(): void {
124
-		$builder = $this->getQueryBuilder();
125
-		$builder->addPartition(new PartitionSplit('filecache', ['filecache']));
126
-
127
-		// query borrowed from UserMountCache
128
-		$query = $builder->select('path')
129
-			->from('filecache')
130
-			->where($builder->expr()->eq('storage', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT)));
131
-
132
-		$results = $query->executeQuery()->fetchAll();
133
-		$this->assertCount(1, $results);
134
-		$this->assertEquals($results[0]['path'], 'file1');
135
-	}
136
-
137
-	public function testSimplePartitionedQuery(): void {
138
-		$builder = $this->getQueryBuilder();
139
-		$builder->addPartition(new PartitionSplit('filecache', ['filecache']));
140
-
141
-		// query borrowed from UserMountCache
142
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_point_hash', 'mount_id', 'f.path', 'mount_provider_class')
143
-			->from('mounts', 'm')
144
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
145
-			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT)));
146
-
147
-		$query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter('partitioned_test')));
148
-
149
-		$this->assertEquals(2, $query->getPartitionCount());
150
-
151
-		$results = $query->executeQuery()->fetchAll();
152
-		$this->assertCount(1, $results);
153
-		$this->assertEquals($results[0]['user_id'], 'partitioned_test');
154
-		$this->assertEquals($results[0]['mount_point'], '/mount/point');
155
-		$this->assertEquals($results[0]['mount_point_hash'], hash('xxh128', '/mount/point'));
156
-		$this->assertEquals($results[0]['mount_provider_class'], 'test');
157
-		$this->assertEquals($results[0]['path'], 'file1');
158
-	}
159
-
160
-	public function testMultiTablePartitionedQuery(): void {
161
-		$builder = $this->getQueryBuilder();
162
-		$builder->addPartition(new PartitionSplit('filecache', ['filecache', 'filecache_extended']));
163
-
164
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_point_hash', 'mount_id', 'f.path', 'mount_provider_class', 'fe.upload_time')
165
-			->from('mounts', 'm')
166
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
167
-			->innerJoin('f', 'filecache_extended', 'fe', $builder->expr()->eq('f.fileid', 'fe.fileid'))
168
-			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT)));
169
-
170
-		$query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter('partitioned_test')));
171
-
172
-		$this->assertEquals(2, $query->getPartitionCount());
173
-
174
-		$results = $query->executeQuery()->fetchAll();
175
-		$this->assertCount(1, $results);
176
-		$this->assertEquals($results[0]['user_id'], 'partitioned_test');
177
-		$this->assertEquals($results[0]['mount_point'], '/mount/point');
178
-		$this->assertEquals($results[0]['mount_point_hash'], hash('xxh128', '/mount/point'));
179
-		$this->assertEquals($results[0]['mount_provider_class'], 'test');
180
-		$this->assertEquals($results[0]['path'], 'file1');
181
-		$this->assertEquals($results[0]['upload_time'], 1234);
182
-	}
183
-
184
-	public function testPartitionedQueryFromSplit(): void {
185
-		$builder = $this->getQueryBuilder();
186
-		$builder->addPartition(new PartitionSplit('filecache', ['filecache']));
187
-
188
-		$query = $builder->select('storage', 'm.root_id', 'm.user_id', 'm.mount_point', 'm.mount_point_hash', 'm.mount_id', 'path', 'm.mount_provider_class')
189
-			->from('filecache', 'f')
190
-			->innerJoin('f', 'mounts', 'm', $builder->expr()->eq('m.root_id', 'f.fileid'));
191
-		$query->where($builder->expr()->eq('storage', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT)));
192
-
193
-		$query->andWhere($builder->expr()->eq('m.user_id', $builder->createNamedParameter('partitioned_test')));
194
-
195
-		$this->assertEquals(2, $query->getPartitionCount());
196
-
197
-		$results = $query->executeQuery()->fetchAll();
198
-		$this->assertCount(1, $results);
199
-		$this->assertEquals($results[0]['user_id'], 'partitioned_test');
200
-		$this->assertEquals($results[0]['mount_point'], '/mount/point');
201
-		$this->assertEquals($results[0]['mount_point_hash'], hash('xxh128', '/mount/point'));
202
-		$this->assertEquals($results[0]['mount_provider_class'], 'test');
203
-		$this->assertEquals($results[0]['path'], 'file1');
204
-	}
205
-
206
-	public function testMultiJoinPartitionedQuery(): void {
207
-		$builder = $this->getQueryBuilder();
208
-		$builder->addPartition(new PartitionSplit('filecache', ['filecache']));
209
-
210
-		// query borrowed from UserMountCache
211
-		$query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_point_hash', 'mount_id', 'f.path', 'mount_provider_class')
212
-			->selectAlias('s.id', 'storage_string_id')
213
-			->from('mounts', 'm')
214
-			->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
215
-			->innerJoin('f', 'storages', 's', $builder->expr()->eq('f.storage', 's.numeric_id'))
216
-			->where($builder->expr()->eq('storage_id', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT)));
217
-
218
-		$query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter('partitioned_test')));
219
-
220
-		$this->assertEquals(3, $query->getPartitionCount());
221
-
222
-		$results = $query->executeQuery()->fetchAll();
223
-		$this->assertCount(1, $results);
224
-		$this->assertEquals($results[0]['user_id'], 'partitioned_test');
225
-		$this->assertEquals($results[0]['mount_point'], '/mount/point');
226
-		$this->assertEquals($results[0]['mount_point_hash'], hash('xxh128', '/mount/point'));
227
-		$this->assertEquals($results[0]['mount_provider_class'], 'test');
228
-		$this->assertEquals($results[0]['path'], 'file1');
229
-		$this->assertEquals($results[0]['storage_string_id'], 'test1');
230
-	}
23
+    private IDBConnection $connection;
24
+    private ShardConnectionManager $shardConnectionManager;
25
+    private AutoIncrementHandler $autoIncrementHandler;
26
+
27
+    protected function setUp(): void {
28
+        if (PHP_INT_SIZE < 8) {
29
+            $this->markTestSkipped('Test requires 64bit');
30
+            return;
31
+        }
32
+        $this->connection = Server::get(IDBConnection::class);
33
+        $this->shardConnectionManager = Server::get(ShardConnectionManager::class);
34
+        $this->autoIncrementHandler = Server::get(AutoIncrementHandler::class);
35
+
36
+        $this->setupFileCache();
37
+    }
38
+
39
+    protected function tearDown(): void {
40
+        // PHP unit also runs tearDown when the test is skipped, but we only initialized when using 64bit
41
+        // see https://github.com/sebastianbergmann/phpunit/issues/6394
42
+        if (PHP_INT_SIZE >= 8) {
43
+            $this->cleanupDb();
44
+        }
45
+        parent::tearDown();
46
+    }
47
+
48
+
49
+    private function getQueryBuilder(): PartitionedQueryBuilder {
50
+        $builder = $this->connection->getQueryBuilder();
51
+        if ($builder instanceof PartitionedQueryBuilder) {
52
+            return $builder;
53
+        } else {
54
+            return new PartitionedQueryBuilder($builder, [], $this->shardConnectionManager, $this->autoIncrementHandler);
55
+        }
56
+    }
57
+
58
+    private function setupFileCache(): void {
59
+        $this->cleanupDb();
60
+        $query = $this->getQueryBuilder();
61
+        $query->insert('storages')
62
+            ->values([
63
+                'numeric_id' => $query->createNamedParameter(1001001, IQueryBuilder::PARAM_INT),
64
+                'id' => $query->createNamedParameter('test1'),
65
+            ]);
66
+        $query->executeStatement();
67
+
68
+        $query = $this->getQueryBuilder();
69
+        $query->insert('filecache')
70
+            ->values([
71
+                'storage' => $query->createNamedParameter(1001001, IQueryBuilder::PARAM_INT),
72
+                'path' => $query->createNamedParameter('file1'),
73
+                'path_hash' => $query->createNamedParameter(md5('file1')),
74
+            ]);
75
+        $query->executeStatement();
76
+        $fileId = $query->getLastInsertId();
77
+
78
+        $query = $this->getQueryBuilder();
79
+        $query->insert('filecache_extended')
80
+            ->hintShardKey('storage', 1001001)
81
+            ->values([
82
+                'fileid' => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT),
83
+                'upload_time' => $query->createNamedParameter(1234, IQueryBuilder::PARAM_INT),
84
+            ]);
85
+        $query->executeStatement();
86
+
87
+        $query = $this->getQueryBuilder();
88
+        $query->insert('mounts')
89
+            ->values([
90
+                'storage_id' => $query->createNamedParameter(1001001, IQueryBuilder::PARAM_INT),
91
+                'user_id' => $query->createNamedParameter('partitioned_test'),
92
+                'mount_point' => $query->createNamedParameter('/mount/point'),
93
+                'mount_point_hash' => $query->createNamedParameter(hash('xxh128', '/mount/point')),
94
+                'mount_provider_class' => $query->createNamedParameter('test'),
95
+                'root_id' => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT),
96
+            ]);
97
+        $query->executeStatement();
98
+    }
99
+
100
+    private function cleanupDb(): void {
101
+        $query = $this->getQueryBuilder();
102
+        $query->delete('storages')
103
+            ->where($query->expr()->gt('numeric_id', $query->createNamedParameter(1000000, IQueryBuilder::PARAM_INT)));
104
+        $query->executeStatement();
105
+
106
+        $query = $this->getQueryBuilder();
107
+        $query->delete('filecache')
108
+            ->where($query->expr()->gt('storage', $query->createNamedParameter(1000000, IQueryBuilder::PARAM_INT)))
109
+            ->runAcrossAllShards();
110
+        $query->executeStatement();
111
+
112
+        $query = $this->getQueryBuilder();
113
+        $query->delete('filecache_extended')
114
+            ->runAcrossAllShards();
115
+        $query->executeStatement();
116
+
117
+        $query = $this->getQueryBuilder();
118
+        $query->delete('mounts')
119
+            ->where($query->expr()->like('user_id', $query->createNamedParameter('partitioned_%')));
120
+        $query->executeStatement();
121
+    }
122
+
123
+    public function testSimpleOnlyPartitionQuery(): void {
124
+        $builder = $this->getQueryBuilder();
125
+        $builder->addPartition(new PartitionSplit('filecache', ['filecache']));
126
+
127
+        // query borrowed from UserMountCache
128
+        $query = $builder->select('path')
129
+            ->from('filecache')
130
+            ->where($builder->expr()->eq('storage', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT)));
131
+
132
+        $results = $query->executeQuery()->fetchAll();
133
+        $this->assertCount(1, $results);
134
+        $this->assertEquals($results[0]['path'], 'file1');
135
+    }
136
+
137
+    public function testSimplePartitionedQuery(): void {
138
+        $builder = $this->getQueryBuilder();
139
+        $builder->addPartition(new PartitionSplit('filecache', ['filecache']));
140
+
141
+        // query borrowed from UserMountCache
142
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_point_hash', 'mount_id', 'f.path', 'mount_provider_class')
143
+            ->from('mounts', 'm')
144
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
145
+            ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT)));
146
+
147
+        $query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter('partitioned_test')));
148
+
149
+        $this->assertEquals(2, $query->getPartitionCount());
150
+
151
+        $results = $query->executeQuery()->fetchAll();
152
+        $this->assertCount(1, $results);
153
+        $this->assertEquals($results[0]['user_id'], 'partitioned_test');
154
+        $this->assertEquals($results[0]['mount_point'], '/mount/point');
155
+        $this->assertEquals($results[0]['mount_point_hash'], hash('xxh128', '/mount/point'));
156
+        $this->assertEquals($results[0]['mount_provider_class'], 'test');
157
+        $this->assertEquals($results[0]['path'], 'file1');
158
+    }
159
+
160
+    public function testMultiTablePartitionedQuery(): void {
161
+        $builder = $this->getQueryBuilder();
162
+        $builder->addPartition(new PartitionSplit('filecache', ['filecache', 'filecache_extended']));
163
+
164
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_point_hash', 'mount_id', 'f.path', 'mount_provider_class', 'fe.upload_time')
165
+            ->from('mounts', 'm')
166
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
167
+            ->innerJoin('f', 'filecache_extended', 'fe', $builder->expr()->eq('f.fileid', 'fe.fileid'))
168
+            ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT)));
169
+
170
+        $query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter('partitioned_test')));
171
+
172
+        $this->assertEquals(2, $query->getPartitionCount());
173
+
174
+        $results = $query->executeQuery()->fetchAll();
175
+        $this->assertCount(1, $results);
176
+        $this->assertEquals($results[0]['user_id'], 'partitioned_test');
177
+        $this->assertEquals($results[0]['mount_point'], '/mount/point');
178
+        $this->assertEquals($results[0]['mount_point_hash'], hash('xxh128', '/mount/point'));
179
+        $this->assertEquals($results[0]['mount_provider_class'], 'test');
180
+        $this->assertEquals($results[0]['path'], 'file1');
181
+        $this->assertEquals($results[0]['upload_time'], 1234);
182
+    }
183
+
184
+    public function testPartitionedQueryFromSplit(): void {
185
+        $builder = $this->getQueryBuilder();
186
+        $builder->addPartition(new PartitionSplit('filecache', ['filecache']));
187
+
188
+        $query = $builder->select('storage', 'm.root_id', 'm.user_id', 'm.mount_point', 'm.mount_point_hash', 'm.mount_id', 'path', 'm.mount_provider_class')
189
+            ->from('filecache', 'f')
190
+            ->innerJoin('f', 'mounts', 'm', $builder->expr()->eq('m.root_id', 'f.fileid'));
191
+        $query->where($builder->expr()->eq('storage', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT)));
192
+
193
+        $query->andWhere($builder->expr()->eq('m.user_id', $builder->createNamedParameter('partitioned_test')));
194
+
195
+        $this->assertEquals(2, $query->getPartitionCount());
196
+
197
+        $results = $query->executeQuery()->fetchAll();
198
+        $this->assertCount(1, $results);
199
+        $this->assertEquals($results[0]['user_id'], 'partitioned_test');
200
+        $this->assertEquals($results[0]['mount_point'], '/mount/point');
201
+        $this->assertEquals($results[0]['mount_point_hash'], hash('xxh128', '/mount/point'));
202
+        $this->assertEquals($results[0]['mount_provider_class'], 'test');
203
+        $this->assertEquals($results[0]['path'], 'file1');
204
+    }
205
+
206
+    public function testMultiJoinPartitionedQuery(): void {
207
+        $builder = $this->getQueryBuilder();
208
+        $builder->addPartition(new PartitionSplit('filecache', ['filecache']));
209
+
210
+        // query borrowed from UserMountCache
211
+        $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_point_hash', 'mount_id', 'f.path', 'mount_provider_class')
212
+            ->selectAlias('s.id', 'storage_string_id')
213
+            ->from('mounts', 'm')
214
+            ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
215
+            ->innerJoin('f', 'storages', 's', $builder->expr()->eq('f.storage', 's.numeric_id'))
216
+            ->where($builder->expr()->eq('storage_id', $builder->createNamedParameter(1001001, IQueryBuilder::PARAM_INT)));
217
+
218
+        $query->andWhere($builder->expr()->eq('user_id', $builder->createNamedParameter('partitioned_test')));
219
+
220
+        $this->assertEquals(3, $query->getPartitionCount());
221
+
222
+        $results = $query->executeQuery()->fetchAll();
223
+        $this->assertCount(1, $results);
224
+        $this->assertEquals($results[0]['user_id'], 'partitioned_test');
225
+        $this->assertEquals($results[0]['mount_point'], '/mount/point');
226
+        $this->assertEquals($results[0]['mount_point_hash'], hash('xxh128', '/mount/point'));
227
+        $this->assertEquals($results[0]['mount_provider_class'], 'test');
228
+        $this->assertEquals($results[0]['path'], 'file1');
229
+        $this->assertEquals($results[0]['storage_string_id'], 'test1');
230
+    }
231 231
 }
Please login to merge, or discard this patch.
core/Migrations/Version33000Date20251209123503.php 1 patch
Indentation   +28 added lines, -28 removed lines patch added patch discarded remove patch
@@ -18,32 +18,32 @@
 block discarded – undo
18 18
 use Override;
19 19
 
20 20
 class Version33000Date20251209123503 extends SimpleMigrationStep {
21
-	public function __construct(
22
-		private readonly IDBConnection $connection,
23
-	) {
24
-	}
25
-
26
-	#[Override]
27
-	public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
28
-		$this->connection->truncateTable('mounts', false);
29
-	}
30
-
31
-	#[Override]
32
-	public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
33
-		/** @var ISchemaWrapper $schema */
34
-		$schema = $schemaClosure();
35
-
36
-		$table = $schema->getTable('mounts');
37
-		if (!$table->hasColumn('mount_point_hash')) {
38
-			$table->addColumn('mount_point_hash', Types::STRING, [
39
-				'notnull' => true,
40
-				'length' => 32, // xxh128
41
-			]);
42
-			$table->dropIndex('mounts_user_root_path_index');
43
-			$table->addUniqueIndex(['user_id', 'root_id', 'mount_point_hash'], 'mounts_user_root_path_index');
44
-			return $schema;
45
-		}
46
-
47
-		return null;
48
-	}
21
+    public function __construct(
22
+        private readonly IDBConnection $connection,
23
+    ) {
24
+    }
25
+
26
+    #[Override]
27
+    public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
28
+        $this->connection->truncateTable('mounts', false);
29
+    }
30
+
31
+    #[Override]
32
+    public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
33
+        /** @var ISchemaWrapper $schema */
34
+        $schema = $schemaClosure();
35
+
36
+        $table = $schema->getTable('mounts');
37
+        if (!$table->hasColumn('mount_point_hash')) {
38
+            $table->addColumn('mount_point_hash', Types::STRING, [
39
+                'notnull' => true,
40
+                'length' => 32, // xxh128
41
+            ]);
42
+            $table->dropIndex('mounts_user_root_path_index');
43
+            $table->addUniqueIndex(['user_id', 'root_id', 'mount_point_hash'], 'mounts_user_root_path_index');
44
+            return $schema;
45
+        }
46
+
47
+        return null;
48
+    }
49 49
 }
Please login to merge, or discard this patch.
core/Listener/AddMissingIndicesListener.php 1 patch
Indentation   +193 added lines, -193 removed lines patch added patch discarded remove patch
@@ -18,197 +18,197 @@
 block discarded – undo
18 18
  */
19 19
 class AddMissingIndicesListener implements IEventListener {
20 20
 
21
-	public function handle(Event $event): void {
22
-		if (!($event instanceof AddMissingIndicesEvent)) {
23
-			return;
24
-		}
25
-
26
-		$event->addMissingIndex(
27
-			'share',
28
-			'share_with_index',
29
-			['share_with']
30
-		);
31
-		$event->addMissingIndex(
32
-			'share',
33
-			'parent_index',
34
-			['parent']
35
-		);
36
-		$event->addMissingIndex(
37
-			'share',
38
-			'owner_index',
39
-			['uid_owner']
40
-		);
41
-		$event->addMissingIndex(
42
-			'share',
43
-			'initiator_index',
44
-			['uid_initiator']
45
-		);
46
-
47
-		$event->addMissingIndex(
48
-			'filecache',
49
-			'fs_mtime',
50
-			['mtime']
51
-		);
52
-		$event->addMissingIndex(
53
-			'filecache',
54
-			'fs_size',
55
-			['size']
56
-		);
57
-		$event->addMissingIndex(
58
-			'filecache',
59
-			'fs_storage_path_prefix',
60
-			['storage', 'path'],
61
-			['lengths' => [null, 64]]
62
-		);
63
-		$event->addMissingIndex(
64
-			'filecache',
65
-			'fs_parent',
66
-			['parent']
67
-		);
68
-		$event->addMissingIndex(
69
-			'filecache',
70
-			'fs_name_hash',
71
-			['name']
72
-		);
73
-
74
-		$event->addMissingIndex(
75
-			'twofactor_providers',
76
-			'twofactor_providers_uid',
77
-			['uid']
78
-		);
79
-
80
-		$event->addMissingUniqueIndex(
81
-			'login_flow_v2',
82
-			'poll_token',
83
-			['poll_token'],
84
-			[],
85
-			true
86
-		);
87
-		$event->addMissingUniqueIndex(
88
-			'login_flow_v2',
89
-			'login_token',
90
-			['login_token'],
91
-			[],
92
-			true
93
-		);
94
-		$event->addMissingIndex(
95
-			'login_flow_v2',
96
-			'timestamp',
97
-			['timestamp'],
98
-			[],
99
-			true
100
-		);
101
-
102
-		$event->addMissingIndex(
103
-			'whats_new',
104
-			'version',
105
-			['version'],
106
-			[],
107
-			true
108
-		);
109
-
110
-		$event->addMissingIndex(
111
-			'cards',
112
-			'cards_abiduri',
113
-			['addressbookid', 'uri'],
114
-			[],
115
-			true
116
-		);
117
-
118
-		$event->replaceIndex(
119
-			'cards_properties',
120
-			['cards_prop_abid'],
121
-			'cards_prop_abid_name_value',
122
-			['addressbookid', 'name', 'value'],
123
-			false,
124
-		);
125
-
126
-		$event->addMissingIndex(
127
-			'calendarobjects_props',
128
-			'calendarobject_calid_index',
129
-			['calendarid', 'calendartype']
130
-		);
131
-
132
-		$event->addMissingIndex(
133
-			'schedulingobjects',
134
-			'schedulobj_principuri_index',
135
-			['principaluri']
136
-		);
137
-
138
-		$event->addMissingIndex(
139
-			'schedulingobjects',
140
-			'schedulobj_lastmodified_idx',
141
-			['lastmodified']
142
-		);
143
-
144
-		$event->addMissingIndex(
145
-			'properties',
146
-			'properties_path_index',
147
-			['userid', 'propertypath']
148
-		);
149
-		$event->addMissingIndex(
150
-			'properties',
151
-			'properties_pathonly_index',
152
-			['propertypath']
153
-		);
154
-		$event->addMissingIndex(
155
-			'properties',
156
-			'properties_name_path_user',
157
-			['propertyname', 'propertypath', 'userid']
158
-		);
159
-
160
-
161
-		$event->addMissingIndex(
162
-			'jobs',
163
-			'job_lastcheck_reserved',
164
-			['last_checked', 'reserved_at']
165
-		);
166
-
167
-		$event->addMissingIndex(
168
-			'direct_edit',
169
-			'direct_edit_timestamp',
170
-			['timestamp']
171
-		);
172
-
173
-		$event->addMissingIndex(
174
-			'preferences',
175
-			'prefs_uid_lazy_i',
176
-			['userid', 'lazy']
177
-		);
178
-		$event->addMissingIndex(
179
-			'preferences',
180
-			'prefs_app_key_ind_fl_i',
181
-			['appid', 'configkey', 'indexed', 'flags']
182
-		);
183
-
184
-		$event->addMissingIndex(
185
-			'mounts',
186
-			'mounts_class_index',
187
-			['mount_provider_class']
188
-		);
189
-
190
-		$event->addMissingIndex(
191
-			'systemtag_object_mapping',
192
-			'systag_by_tagid',
193
-			['systemtagid', 'objecttype']
194
-		);
195
-
196
-		$event->addMissingIndex(
197
-			'systemtag_object_mapping',
198
-			'systag_by_objectid',
199
-			['objectid']
200
-		);
201
-
202
-		$event->addMissingIndex(
203
-			'systemtag_object_mapping',
204
-			'systag_objecttype',
205
-			['objecttype']
206
-		);
207
-
208
-		$event->addMissingUniqueIndex(
209
-			'vcategory',
210
-			'unique_category_per_user',
211
-			['uid', 'type', 'category']
212
-		);
213
-	}
21
+    public function handle(Event $event): void {
22
+        if (!($event instanceof AddMissingIndicesEvent)) {
23
+            return;
24
+        }
25
+
26
+        $event->addMissingIndex(
27
+            'share',
28
+            'share_with_index',
29
+            ['share_with']
30
+        );
31
+        $event->addMissingIndex(
32
+            'share',
33
+            'parent_index',
34
+            ['parent']
35
+        );
36
+        $event->addMissingIndex(
37
+            'share',
38
+            'owner_index',
39
+            ['uid_owner']
40
+        );
41
+        $event->addMissingIndex(
42
+            'share',
43
+            'initiator_index',
44
+            ['uid_initiator']
45
+        );
46
+
47
+        $event->addMissingIndex(
48
+            'filecache',
49
+            'fs_mtime',
50
+            ['mtime']
51
+        );
52
+        $event->addMissingIndex(
53
+            'filecache',
54
+            'fs_size',
55
+            ['size']
56
+        );
57
+        $event->addMissingIndex(
58
+            'filecache',
59
+            'fs_storage_path_prefix',
60
+            ['storage', 'path'],
61
+            ['lengths' => [null, 64]]
62
+        );
63
+        $event->addMissingIndex(
64
+            'filecache',
65
+            'fs_parent',
66
+            ['parent']
67
+        );
68
+        $event->addMissingIndex(
69
+            'filecache',
70
+            'fs_name_hash',
71
+            ['name']
72
+        );
73
+
74
+        $event->addMissingIndex(
75
+            'twofactor_providers',
76
+            'twofactor_providers_uid',
77
+            ['uid']
78
+        );
79
+
80
+        $event->addMissingUniqueIndex(
81
+            'login_flow_v2',
82
+            'poll_token',
83
+            ['poll_token'],
84
+            [],
85
+            true
86
+        );
87
+        $event->addMissingUniqueIndex(
88
+            'login_flow_v2',
89
+            'login_token',
90
+            ['login_token'],
91
+            [],
92
+            true
93
+        );
94
+        $event->addMissingIndex(
95
+            'login_flow_v2',
96
+            'timestamp',
97
+            ['timestamp'],
98
+            [],
99
+            true
100
+        );
101
+
102
+        $event->addMissingIndex(
103
+            'whats_new',
104
+            'version',
105
+            ['version'],
106
+            [],
107
+            true
108
+        );
109
+
110
+        $event->addMissingIndex(
111
+            'cards',
112
+            'cards_abiduri',
113
+            ['addressbookid', 'uri'],
114
+            [],
115
+            true
116
+        );
117
+
118
+        $event->replaceIndex(
119
+            'cards_properties',
120
+            ['cards_prop_abid'],
121
+            'cards_prop_abid_name_value',
122
+            ['addressbookid', 'name', 'value'],
123
+            false,
124
+        );
125
+
126
+        $event->addMissingIndex(
127
+            'calendarobjects_props',
128
+            'calendarobject_calid_index',
129
+            ['calendarid', 'calendartype']
130
+        );
131
+
132
+        $event->addMissingIndex(
133
+            'schedulingobjects',
134
+            'schedulobj_principuri_index',
135
+            ['principaluri']
136
+        );
137
+
138
+        $event->addMissingIndex(
139
+            'schedulingobjects',
140
+            'schedulobj_lastmodified_idx',
141
+            ['lastmodified']
142
+        );
143
+
144
+        $event->addMissingIndex(
145
+            'properties',
146
+            'properties_path_index',
147
+            ['userid', 'propertypath']
148
+        );
149
+        $event->addMissingIndex(
150
+            'properties',
151
+            'properties_pathonly_index',
152
+            ['propertypath']
153
+        );
154
+        $event->addMissingIndex(
155
+            'properties',
156
+            'properties_name_path_user',
157
+            ['propertyname', 'propertypath', 'userid']
158
+        );
159
+
160
+
161
+        $event->addMissingIndex(
162
+            'jobs',
163
+            'job_lastcheck_reserved',
164
+            ['last_checked', 'reserved_at']
165
+        );
166
+
167
+        $event->addMissingIndex(
168
+            'direct_edit',
169
+            'direct_edit_timestamp',
170
+            ['timestamp']
171
+        );
172
+
173
+        $event->addMissingIndex(
174
+            'preferences',
175
+            'prefs_uid_lazy_i',
176
+            ['userid', 'lazy']
177
+        );
178
+        $event->addMissingIndex(
179
+            'preferences',
180
+            'prefs_app_key_ind_fl_i',
181
+            ['appid', 'configkey', 'indexed', 'flags']
182
+        );
183
+
184
+        $event->addMissingIndex(
185
+            'mounts',
186
+            'mounts_class_index',
187
+            ['mount_provider_class']
188
+        );
189
+
190
+        $event->addMissingIndex(
191
+            'systemtag_object_mapping',
192
+            'systag_by_tagid',
193
+            ['systemtagid', 'objecttype']
194
+        );
195
+
196
+        $event->addMissingIndex(
197
+            'systemtag_object_mapping',
198
+            'systag_by_objectid',
199
+            ['objectid']
200
+        );
201
+
202
+        $event->addMissingIndex(
203
+            'systemtag_object_mapping',
204
+            'systag_objecttype',
205
+            ['objecttype']
206
+        );
207
+
208
+        $event->addMissingUniqueIndex(
209
+            'vcategory',
210
+            'unique_category_per_user',
211
+            ['uid', 'type', 'category']
212
+        );
213
+    }
214 214
 }
Please login to merge, or discard this patch.