Completed
Push — master ( b3a294...00e126 )
by Joas
30:35 queued 02:34
created
lib/private/Files/Cache/Cache.php 1 patch
Indentation   +1261 added lines, -1261 removed lines patch added patch discarded remove patch
@@ -47,1265 +47,1265 @@
 block discarded – undo
47 47
  * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
48 48
  */
49 49
 class Cache implements ICache {
50
-	use MoveFromCacheTrait {
51
-		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
52
-	}
53
-
54
-	/**
55
-	 * @var array partial data for the cache
56
-	 */
57
-	protected array $partial = [];
58
-	protected string $storageId;
59
-	protected Storage $storageCache;
60
-	protected IMimeTypeLoader $mimetypeLoader;
61
-	protected IDBConnection $connection;
62
-	protected SystemConfig $systemConfig;
63
-	protected LoggerInterface $logger;
64
-	protected QuerySearchHelper $querySearchHelper;
65
-	protected IEventDispatcher $eventDispatcher;
66
-	protected IFilesMetadataManager $metadataManager;
67
-
68
-	public function __construct(
69
-		private IStorage $storage,
70
-		// this constructor is used in to many pleases to easily do proper di
71
-		// so instead we group it all together
72
-		?CacheDependencies $dependencies = null,
73
-	) {
74
-		$this->storageId = $storage->getId();
75
-		if (strlen($this->storageId) > 64) {
76
-			$this->storageId = md5($this->storageId);
77
-		}
78
-		if (!$dependencies) {
79
-			$dependencies = \OCP\Server::get(CacheDependencies::class);
80
-		}
81
-		$this->storageCache = new Storage($this->storage, true, $dependencies->getConnection());
82
-		$this->mimetypeLoader = $dependencies->getMimeTypeLoader();
83
-		$this->connection = $dependencies->getConnection();
84
-		$this->systemConfig = $dependencies->getSystemConfig();
85
-		$this->logger = $dependencies->getLogger();
86
-		$this->querySearchHelper = $dependencies->getQuerySearchHelper();
87
-		$this->eventDispatcher = $dependencies->getEventDispatcher();
88
-		$this->metadataManager = $dependencies->getMetadataManager();
89
-	}
90
-
91
-	protected function getQueryBuilder() {
92
-		return new CacheQueryBuilder(
93
-			$this->connection->getQueryBuilder(),
94
-			$this->metadataManager,
95
-		);
96
-	}
97
-
98
-	public function getStorageCache(): Storage {
99
-		return $this->storageCache;
100
-	}
101
-
102
-	/**
103
-	 * Get the numeric storage id for this cache's storage
104
-	 *
105
-	 * @return int
106
-	 */
107
-	public function getNumericStorageId() {
108
-		return $this->storageCache->getNumericId();
109
-	}
110
-
111
-	/**
112
-	 * get the stored metadata of a file or folder
113
-	 *
114
-	 * @param string|int $file either the path of a file or folder or the file id for a file or folder
115
-	 * @return ICacheEntry|false the cache entry as array or false if the file is not found in the cache
116
-	 */
117
-	public function get($file) {
118
-		$query = $this->getQueryBuilder();
119
-		$query->selectFileCache();
120
-		$metadataQuery = $query->selectMetadata();
121
-
122
-		if (is_string($file) || $file == '') {
123
-			// normalize file
124
-			$file = $this->normalize($file);
125
-
126
-			$query->wherePath($file);
127
-		} else { //file id
128
-			$query->whereFileId($file);
129
-		}
130
-		$query->whereStorageId($this->getNumericStorageId());
131
-
132
-		$result = $query->executeQuery();
133
-		$data = $result->fetch();
134
-		$result->closeCursor();
135
-
136
-		if ($data !== false) {
137
-			$data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
138
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
139
-		} else {
140
-			//merge partial data
141
-			if (is_string($file) && isset($this->partial[$file])) {
142
-				return $this->partial[$file];
143
-			}
144
-		}
145
-
146
-		return false;
147
-	}
148
-
149
-	/**
150
-	 * Create a CacheEntry from database row
151
-	 *
152
-	 * @param array $data
153
-	 * @param IMimeTypeLoader $mimetypeLoader
154
-	 * @return CacheEntry
155
-	 */
156
-	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
157
-		//fix types
158
-		$data['name'] = (string)$data['name'];
159
-		$data['path'] = (string)$data['path'];
160
-		$data['fileid'] = (int)$data['fileid'];
161
-		$data['parent'] = (int)$data['parent'];
162
-		$data['size'] = Util::numericToNumber($data['size']);
163
-		$data['unencrypted_size'] = Util::numericToNumber($data['unencrypted_size'] ?? 0);
164
-		$data['mtime'] = (int)$data['mtime'];
165
-		$data['storage_mtime'] = (int)$data['storage_mtime'];
166
-		$data['encryptedVersion'] = (int)$data['encrypted'];
167
-		$data['encrypted'] = (bool)$data['encrypted'];
168
-		$data['storage_id'] = $data['storage'];
169
-		$data['storage'] = (int)$data['storage'];
170
-		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
171
-		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
172
-		if ($data['storage_mtime'] == 0) {
173
-			$data['storage_mtime'] = $data['mtime'];
174
-		}
175
-		if (isset($data['f_permissions'])) {
176
-			$data['scan_permissions'] ??= $data['f_permissions'];
177
-		}
178
-		$data['permissions'] = (int)$data['permissions'];
179
-		if (isset($data['creation_time'])) {
180
-			$data['creation_time'] = (int)$data['creation_time'];
181
-		}
182
-		if (isset($data['upload_time'])) {
183
-			$data['upload_time'] = (int)$data['upload_time'];
184
-		}
185
-		return new CacheEntry($data);
186
-	}
187
-
188
-	/**
189
-	 * get the metadata of all files stored in $folder
190
-	 *
191
-	 * @param string $folder
192
-	 * @return ICacheEntry[]
193
-	 */
194
-	public function getFolderContents($folder) {
195
-		$fileId = $this->getId($folder);
196
-		return $this->getFolderContentsById($fileId);
197
-	}
198
-
199
-	/**
200
-	 * get the metadata of all files stored in $folder
201
-	 *
202
-	 * @param int $fileId the file id of the folder
203
-	 * @return ICacheEntry[]
204
-	 */
205
-	public function getFolderContentsById($fileId) {
206
-		if ($fileId > -1) {
207
-			$query = $this->getQueryBuilder();
208
-			$query->selectFileCache()
209
-				->whereParent($fileId)
210
-				->whereStorageId($this->getNumericStorageId())
211
-				->orderBy('name', 'ASC');
212
-
213
-			$metadataQuery = $query->selectMetadata();
214
-
215
-			$result = $query->executeQuery();
216
-			$files = $result->fetchAll();
217
-			$result->closeCursor();
218
-
219
-			return array_map(function (array $data) use ($metadataQuery) {
220
-				$data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
221
-				return self::cacheEntryFromData($data, $this->mimetypeLoader);
222
-			}, $files);
223
-		}
224
-		return [];
225
-	}
226
-
227
-	/**
228
-	 * insert or update meta data for a file or folder
229
-	 *
230
-	 * @param string $file
231
-	 * @param array $data
232
-	 *
233
-	 * @return int file id
234
-	 * @throws \RuntimeException
235
-	 */
236
-	public function put($file, array $data) {
237
-		if (($id = $this->getId($file)) > -1) {
238
-			$this->update($id, $data);
239
-			return $id;
240
-		} else {
241
-			return $this->insert($file, $data);
242
-		}
243
-	}
244
-
245
-	/**
246
-	 * insert meta data for a new file or folder
247
-	 *
248
-	 * @param string $file
249
-	 * @param array $data
250
-	 *
251
-	 * @return int file id
252
-	 * @throws \RuntimeException|Exception
253
-	 */
254
-	public function insert($file, array $data) {
255
-		// normalize file
256
-		$file = $this->normalize($file);
257
-
258
-		if (isset($this->partial[$file])) { //add any saved partial data
259
-			$data = array_merge($this->partial[$file]->getData(), $data);
260
-			unset($this->partial[$file]);
261
-		}
262
-
263
-		$requiredFields = ['size', 'mtime', 'mimetype'];
264
-		foreach ($requiredFields as $field) {
265
-			if (!isset($data[$field])) { //data not complete save as partial and return
266
-				$this->partial[$file] = new CacheEntry($data);
267
-				return -1;
268
-			}
269
-		}
270
-
271
-		$data['path'] = $file;
272
-		if (!isset($data['parent'])) {
273
-			$data['parent'] = $this->getParentId($file);
274
-		}
275
-		if ($data['parent'] === -1 && $file !== '') {
276
-			throw new \Exception('Parent folder not in filecache for ' . $file);
277
-		}
278
-		$data['name'] = basename($file);
279
-
280
-		[$values, $extensionValues] = $this->normalizeData($data);
281
-		$storageId = $this->getNumericStorageId();
282
-		$values['storage'] = $storageId;
283
-
284
-		try {
285
-			$builder = $this->connection->getQueryBuilder();
286
-			$builder->insert('filecache');
287
-
288
-			foreach ($values as $column => $value) {
289
-				$builder->setValue($column, $builder->createNamedParameter($value));
290
-			}
291
-
292
-			if ($builder->executeStatement()) {
293
-				$fileId = $builder->getLastInsertId();
294
-
295
-				if (count($extensionValues)) {
296
-					$query = $this->getQueryBuilder();
297
-					$query->insert('filecache_extended');
298
-					$query->hintShardKey('storage', $storageId);
299
-
300
-					$query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
301
-					foreach ($extensionValues as $column => $value) {
302
-						$query->setValue($column, $query->createNamedParameter($value));
303
-					}
304
-					$query->executeStatement();
305
-				}
306
-
307
-				$event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
308
-				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
309
-				$this->eventDispatcher->dispatchTyped($event);
310
-				return $fileId;
311
-			}
312
-		} catch (Exception $e) {
313
-			if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
314
-				// entry exists already
315
-				if ($this->connection->inTransaction()) {
316
-					$this->connection->commit();
317
-					$this->connection->beginTransaction();
318
-				}
319
-			} else {
320
-				throw $e;
321
-			}
322
-		}
323
-
324
-		// The file was created in the meantime
325
-		if (($id = $this->getId($file)) > -1) {
326
-			$this->update($id, $data);
327
-			return $id;
328
-		} else {
329
-			throw new \RuntimeException('File entry could not be inserted but could also not be selected with getId() in order to perform an update. Please try again.');
330
-		}
331
-	}
332
-
333
-	/**
334
-	 * update the metadata of an existing file or folder in the cache
335
-	 *
336
-	 * @param int $id the fileid of the existing file or folder
337
-	 * @param array $data [$key => $value] the metadata to update, only the fields provided in the array will be updated, non-provided values will remain unchanged
338
-	 */
339
-	public function update($id, array $data) {
340
-		if (isset($data['path'])) {
341
-			// normalize path
342
-			$data['path'] = $this->normalize($data['path']);
343
-		}
344
-
345
-		if (isset($data['name'])) {
346
-			// normalize path
347
-			$data['name'] = $this->normalize($data['name']);
348
-		}
349
-
350
-		[$values, $extensionValues] = $this->normalizeData($data);
351
-
352
-		if (count($values)) {
353
-			$query = $this->getQueryBuilder();
354
-
355
-			$query->update('filecache')
356
-				->whereFileId($id)
357
-				->whereStorageId($this->getNumericStorageId())
358
-				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
359
-					return $query->expr()->orX(
360
-						$query->expr()->neq($key, $query->createNamedParameter($value)),
361
-						$query->expr()->isNull($key)
362
-					);
363
-				}, array_keys($values), array_values($values))));
364
-
365
-			foreach ($values as $key => $value) {
366
-				$query->set($key, $query->createNamedParameter($value));
367
-			}
368
-
369
-			$query->executeStatement();
370
-		}
371
-
372
-		if (count($extensionValues)) {
373
-			try {
374
-				$query = $this->getQueryBuilder();
375
-				$query->insert('filecache_extended');
376
-				$query->hintShardKey('storage', $this->getNumericStorageId());
377
-
378
-				$query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
379
-				foreach ($extensionValues as $column => $value) {
380
-					$query->setValue($column, $query->createNamedParameter($value));
381
-				}
382
-
383
-				$query->executeStatement();
384
-			} catch (Exception $e) {
385
-				if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
386
-					throw $e;
387
-				}
388
-				$query = $this->getQueryBuilder();
389
-				$query->update('filecache_extended')
390
-					->whereFileId($id)
391
-					->hintShardKey('storage', $this->getNumericStorageId())
392
-					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
393
-						return $query->expr()->orX(
394
-							$query->expr()->neq($key, $query->createNamedParameter($value)),
395
-							$query->expr()->isNull($key)
396
-						);
397
-					}, array_keys($extensionValues), array_values($extensionValues))));
398
-
399
-				foreach ($extensionValues as $key => $value) {
400
-					$query->set($key, $query->createNamedParameter($value));
401
-				}
402
-
403
-				$query->executeStatement();
404
-			}
405
-		}
406
-
407
-		$path = $this->getPathById($id);
408
-		// path can still be null if the file doesn't exist
409
-		if ($path !== null) {
410
-			$event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
411
-			$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
412
-			$this->eventDispatcher->dispatchTyped($event);
413
-		}
414
-	}
415
-
416
-	/**
417
-	 * extract query parts and params array from data array
418
-	 *
419
-	 * @param array $data
420
-	 * @return array
421
-	 */
422
-	protected function normalizeData(array $data): array {
423
-		$fields = [
424
-			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
425
-			'etag', 'permissions', 'checksum', 'storage', 'unencrypted_size'];
426
-		$extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
427
-
428
-		$doNotCopyStorageMTime = false;
429
-		if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
430
-			// this horrific magic tells it to not copy storage_mtime to mtime
431
-			unset($data['mtime']);
432
-			$doNotCopyStorageMTime = true;
433
-		}
434
-
435
-		$params = [];
436
-		$extensionParams = [];
437
-		foreach ($data as $name => $value) {
438
-			if (in_array($name, $fields)) {
439
-				if ($name === 'path') {
440
-					$params['path_hash'] = md5($value);
441
-				} elseif ($name === 'mimetype') {
442
-					$params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
443
-					$value = $this->mimetypeLoader->getId($value);
444
-				} elseif ($name === 'storage_mtime') {
445
-					if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
446
-						$params['mtime'] = $value;
447
-					}
448
-				} elseif ($name === 'encrypted') {
449
-					if (isset($data['encryptedVersion'])) {
450
-						$value = $data['encryptedVersion'];
451
-					} else {
452
-						// Boolean to integer conversion
453
-						$value = $value ? 1 : 0;
454
-					}
455
-				}
456
-				$params[$name] = $value;
457
-			}
458
-			if (in_array($name, $extensionFields)) {
459
-				$extensionParams[$name] = $value;
460
-			}
461
-		}
462
-		return [$params, array_filter($extensionParams)];
463
-	}
464
-
465
-	/**
466
-	 * get the file id for a file
467
-	 *
468
-	 * A file id is a numeric id for a file or folder that's unique within an owncloud instance which stays the same for the lifetime of a file
469
-	 *
470
-	 * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
471
-	 *
472
-	 * @param string $file
473
-	 * @return int
474
-	 */
475
-	public function getId($file) {
476
-		// normalize file
477
-		$file = $this->normalize($file);
478
-
479
-		$query = $this->getQueryBuilder();
480
-		$query->select('fileid')
481
-			->from('filecache')
482
-			->whereStorageId($this->getNumericStorageId())
483
-			->wherePath($file);
484
-
485
-		$result = $query->executeQuery();
486
-		$id = $result->fetchOne();
487
-		$result->closeCursor();
488
-
489
-		return $id === false ? -1 : (int)$id;
490
-	}
491
-
492
-	/**
493
-	 * get the id of the parent folder of a file
494
-	 *
495
-	 * @param string $file
496
-	 * @return int
497
-	 */
498
-	public function getParentId($file) {
499
-		if ($file === '') {
500
-			return -1;
501
-		} else {
502
-			$parent = $this->getParentPath($file);
503
-			return (int)$this->getId($parent);
504
-		}
505
-	}
506
-
507
-	private function getParentPath($path) {
508
-		$parent = dirname($path);
509
-		if ($parent === '.') {
510
-			$parent = '';
511
-		}
512
-		return $parent;
513
-	}
514
-
515
-	/**
516
-	 * check if a file is available in the cache
517
-	 *
518
-	 * @param string $file
519
-	 * @return bool
520
-	 */
521
-	public function inCache($file) {
522
-		return $this->getId($file) != -1;
523
-	}
524
-
525
-	/**
526
-	 * remove a file or folder from the cache
527
-	 *
528
-	 * when removing a folder from the cache all files and folders inside the folder will be removed as well
529
-	 *
530
-	 * @param string $file
531
-	 */
532
-	public function remove($file) {
533
-		$entry = $this->get($file);
534
-
535
-		if ($entry instanceof ICacheEntry) {
536
-			$query = $this->getQueryBuilder();
537
-			$query->delete('filecache')
538
-				->whereStorageId($this->getNumericStorageId())
539
-				->whereFileId($entry->getId());
540
-			$query->executeStatement();
541
-
542
-			$query = $this->getQueryBuilder();
543
-			$query->delete('filecache_extended')
544
-				->whereFileId($entry->getId())
545
-				->hintShardKey('storage', $this->getNumericStorageId());
546
-			$query->executeStatement();
547
-
548
-			if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
549
-				$this->removeChildren($entry);
550
-			}
551
-
552
-			$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
553
-		}
554
-	}
555
-
556
-	/**
557
-	 * Remove all children of a folder
558
-	 *
559
-	 * @param ICacheEntry $entry the cache entry of the folder to remove the children of
560
-	 * @throws \OC\DatabaseException
561
-	 */
562
-	private function removeChildren(ICacheEntry $entry) {
563
-		$parentIds = [$entry->getId()];
564
-		$queue = [$entry->getId()];
565
-		$deletedIds = [];
566
-		$deletedPaths = [];
567
-
568
-		// we walk depth first through the file tree, removing all filecache_extended attributes while we walk
569
-		// and collecting all folder ids to later use to delete the filecache entries
570
-		while ($entryId = array_pop($queue)) {
571
-			$children = $this->getFolderContentsById($entryId);
572
-			$childIds = array_map(function (ICacheEntry $cacheEntry) {
573
-				return $cacheEntry->getId();
574
-			}, $children);
575
-			$childPaths = array_map(function (ICacheEntry $cacheEntry) {
576
-				return $cacheEntry->getPath();
577
-			}, $children);
578
-
579
-			foreach ($childIds as $childId) {
580
-				$deletedIds[] = $childId;
581
-			}
582
-
583
-			foreach ($childPaths as $childPath) {
584
-				$deletedPaths[] = $childPath;
585
-			}
586
-
587
-			$query = $this->getQueryBuilder();
588
-			$query->delete('filecache_extended')
589
-				->where($query->expr()->in('fileid', $query->createParameter('childIds')))
590
-				->hintShardKey('storage', $this->getNumericStorageId());
591
-
592
-			foreach (array_chunk($childIds, 1000) as $childIdChunk) {
593
-				$query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
594
-				$query->executeStatement();
595
-			}
596
-
597
-			/** @var ICacheEntry[] $childFolders */
598
-			$childFolders = [];
599
-			foreach ($children as $child) {
600
-				if ($child->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
601
-					$childFolders[] = $child;
602
-				}
603
-			}
604
-			foreach ($childFolders as $folder) {
605
-				$parentIds[] = $folder->getId();
606
-				$queue[] = $folder->getId();
607
-			}
608
-		}
609
-
610
-		$query = $this->getQueryBuilder();
611
-		$query->delete('filecache')
612
-			->whereStorageId($this->getNumericStorageId())
613
-			->whereParentInParameter('parentIds');
614
-
615
-		// Sorting before chunking allows the db to find the entries close to each
616
-		// other in the index
617
-		sort($parentIds, SORT_NUMERIC);
618
-		foreach (array_chunk($parentIds, 1000) as $parentIdChunk) {
619
-			$query->setParameter('parentIds', $parentIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
620
-			$query->executeStatement();
621
-		}
622
-
623
-		foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) {
624
-			$cacheEntryRemovedEvent = new CacheEntryRemovedEvent(
625
-				$this->storage,
626
-				$filePath,
627
-				$fileId,
628
-				$this->getNumericStorageId()
629
-			);
630
-			$this->eventDispatcher->dispatchTyped($cacheEntryRemovedEvent);
631
-		}
632
-	}
633
-
634
-	/**
635
-	 * Move a file or folder in the cache
636
-	 *
637
-	 * @param string $source
638
-	 * @param string $target
639
-	 */
640
-	public function move($source, $target) {
641
-		$this->moveFromCache($this, $source, $target);
642
-	}
643
-
644
-	/**
645
-	 * Get the storage id and path needed for a move
646
-	 *
647
-	 * @param string $path
648
-	 * @return array [$storageId, $internalPath]
649
-	 */
650
-	protected function getMoveInfo($path) {
651
-		return [$this->getNumericStorageId(), $path];
652
-	}
653
-
654
-	protected function hasEncryptionWrapper(): bool {
655
-		return $this->storage->instanceOfStorage(Encryption::class);
656
-	}
657
-
658
-	protected function shouldEncrypt(string $targetPath): bool {
659
-		if (!$this->storage->instanceOfStorage(Encryption::class)) {
660
-			return false;
661
-		}
662
-		return $this->storage->shouldEncrypt($targetPath);
663
-	}
664
-
665
-	/**
666
-	 * Move a file or folder in the cache
667
-	 *
668
-	 * @param ICache $sourceCache
669
-	 * @param string $sourcePath
670
-	 * @param string $targetPath
671
-	 * @throws \OC\DatabaseException
672
-	 * @throws \Exception if the given storages have an invalid id
673
-	 */
674
-	public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
675
-		if ($sourceCache instanceof Cache) {
676
-			// normalize source and target
677
-			$sourcePath = $this->normalize($sourcePath);
678
-			$targetPath = $this->normalize($targetPath);
679
-
680
-			$sourceData = $sourceCache->get($sourcePath);
681
-			if (!$sourceData) {
682
-				throw new \Exception('Source path not found in cache: ' . $sourcePath);
683
-			}
684
-
685
-			$shardDefinition = $this->connection->getShardDefinition('filecache');
686
-			if (
687
-				$shardDefinition
688
-				&& $shardDefinition->getShardForKey($sourceCache->getNumericStorageId()) !== $shardDefinition->getShardForKey($this->getNumericStorageId())
689
-			) {
690
-				$this->moveFromStorageSharded($shardDefinition, $sourceCache, $sourceData, $targetPath);
691
-				return;
692
-			}
693
-
694
-			$sourceId = $sourceData['fileid'];
695
-			$newParentId = $this->getParentId($targetPath);
696
-
697
-			[$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
698
-			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
699
-
700
-			if (is_null($sourceStorageId) || $sourceStorageId === false) {
701
-				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
702
-			}
703
-			if (is_null($targetStorageId) || $targetStorageId === false) {
704
-				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
705
-			}
706
-
707
-			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
708
-				//update all child entries
709
-				$sourceLength = mb_strlen($sourcePath);
710
-
711
-				$childIds = $this->getChildIds($sourceStorageId, $sourcePath);
712
-
713
-				$childChunks = array_chunk($childIds, 1000);
714
-
715
-				$query = $this->getQueryBuilder();
716
-
717
-				$fun = $query->func();
718
-				$newPathFunction = $fun->concat(
719
-					$query->createNamedParameter($targetPath),
720
-					$fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
721
-				);
722
-				$query->update('filecache')
723
-					->set('path_hash', $fun->md5($newPathFunction))
724
-					->set('path', $newPathFunction)
725
-					->whereStorageId($sourceStorageId)
726
-					->andWhere($query->expr()->in('fileid', $query->createParameter('files')));
727
-
728
-				if ($sourceStorageId !== $targetStorageId) {
729
-					$query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
730
-				}
731
-
732
-				// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
733
-				if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
734
-					$query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
735
-				}
736
-
737
-				// Retry transaction in case of RetryableException like deadlocks.
738
-				// Retry up to 4 times because we should receive up to 4 concurrent requests from the frontend
739
-				$retryLimit = 4;
740
-				for ($i = 1; $i <= $retryLimit; $i++) {
741
-					try {
742
-						$this->connection->beginTransaction();
743
-						foreach ($childChunks as $chunk) {
744
-							$query->setParameter('files', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
745
-							$query->executeStatement();
746
-						}
747
-						break;
748
-					} catch (\OC\DatabaseException $e) {
749
-						$this->connection->rollBack();
750
-						throw $e;
751
-					} catch (DbalException $e) {
752
-						$this->connection->rollBack();
753
-
754
-						if (!$e->isRetryable()) {
755
-							throw $e;
756
-						}
757
-
758
-						// Simply throw if we already retried 4 times.
759
-						if ($i === $retryLimit) {
760
-							throw $e;
761
-						}
762
-
763
-						// Sleep a bit to give some time to the other transaction to finish.
764
-						usleep(100 * 1000 * $i);
765
-					}
766
-				}
767
-			} else {
768
-				$this->connection->beginTransaction();
769
-			}
770
-
771
-			$query = $this->getQueryBuilder();
772
-			$query->update('filecache')
773
-				->set('path', $query->createNamedParameter($targetPath))
774
-				->set('path_hash', $query->createNamedParameter(md5($targetPath)))
775
-				->set('name', $query->createNamedParameter(basename($targetPath)))
776
-				->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
777
-				->whereStorageId($sourceStorageId)
778
-				->whereFileId($sourceId);
779
-
780
-			if ($sourceStorageId !== $targetStorageId) {
781
-				$query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
782
-			}
783
-
784
-			// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
785
-			if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
786
-				$query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
787
-			}
788
-
789
-			$query->executeStatement();
790
-
791
-			$this->connection->commit();
792
-
793
-			if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
794
-				\OCP\Server::get(\OCP\Files\Config\IUserMountCache::class)->clear();
795
-				$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
796
-				$event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
797
-				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
798
-				$this->eventDispatcher->dispatchTyped($event);
799
-			} else {
800
-				$event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
801
-				$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
802
-				$this->eventDispatcher->dispatchTyped($event);
803
-			}
804
-		} else {
805
-			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
806
-		}
807
-	}
808
-
809
-	private function getChildIds(int $storageId, string $path): array {
810
-		$query = $this->connection->getQueryBuilder();
811
-		$query->select('fileid')
812
-			->from('filecache')
813
-			->where($query->expr()->eq('storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
814
-			->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($path) . '/%')));
815
-		return $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
816
-	}
817
-
818
-	/**
819
-	 * remove all entries for files that are stored on the storage from the cache
820
-	 */
821
-	public function clear() {
822
-		$query = $this->getQueryBuilder();
823
-		$query->delete('filecache')
824
-			->whereStorageId($this->getNumericStorageId());
825
-		$query->executeStatement();
826
-
827
-		$query = $this->connection->getQueryBuilder();
828
-		$query->delete('storages')
829
-			->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
830
-		$query->executeStatement();
831
-	}
832
-
833
-	/**
834
-	 * Get the scan status of a file
835
-	 *
836
-	 * - Cache::NOT_FOUND: File is not in the cache
837
-	 * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
838
-	 * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
839
-	 * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
840
-	 *
841
-	 * @param string $file
842
-	 *
843
-	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
844
-	 */
845
-	public function getStatus($file) {
846
-		// normalize file
847
-		$file = $this->normalize($file);
848
-
849
-		$query = $this->getQueryBuilder();
850
-		$query->select('size')
851
-			->from('filecache')
852
-			->whereStorageId($this->getNumericStorageId())
853
-			->wherePath($file);
854
-
855
-		$result = $query->executeQuery();
856
-		$size = $result->fetchOne();
857
-		$result->closeCursor();
858
-
859
-		if ($size !== false) {
860
-			if ((int)$size === -1) {
861
-				return self::SHALLOW;
862
-			} else {
863
-				return self::COMPLETE;
864
-			}
865
-		} else {
866
-			if (isset($this->partial[$file])) {
867
-				return self::PARTIAL;
868
-			} else {
869
-				return self::NOT_FOUND;
870
-			}
871
-		}
872
-	}
873
-
874
-	/**
875
-	 * search for files matching $pattern
876
-	 *
877
-	 * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
878
-	 * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
879
-	 */
880
-	public function search($pattern) {
881
-		$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', $pattern);
882
-		return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
883
-	}
884
-
885
-	/**
886
-	 * search for files by mimetype
887
-	 *
888
-	 * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
889
-	 *                         where it will search for all mimetypes in the group ('image/*')
890
-	 * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
891
-	 */
892
-	public function searchByMime($mimetype) {
893
-		if (!str_contains($mimetype, '/')) {
894
-			$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
895
-		} else {
896
-			$operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
897
-		}
898
-		return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
899
-	}
900
-
901
-	public function searchQuery(ISearchQuery $query) {
902
-		return current($this->querySearchHelper->searchInCaches($query, [$this]));
903
-	}
904
-
905
-	/**
906
-	 * Re-calculate the folder size and the size of all parent folders
907
-	 *
908
-	 * @param array|ICacheEntry|null $data (optional) meta data of the folder
909
-	 */
910
-	public function correctFolderSize(string $path, $data = null, bool $isBackgroundScan = false): void {
911
-		$this->calculateFolderSize($path, $data);
912
-
913
-		if ($path !== '') {
914
-			$parent = dirname($path);
915
-			if ($parent === '.' || $parent === '/') {
916
-				$parent = '';
917
-			}
918
-
919
-			if ($isBackgroundScan) {
920
-				$parentData = $this->get($parent);
921
-				if ($parentData !== false
922
-					&& $parentData['size'] !== -1
923
-					&& $this->getIncompleteChildrenCount($parentData['fileid']) === 0
924
-				) {
925
-					$this->correctFolderSize($parent, $parentData, $isBackgroundScan);
926
-				}
927
-			} else {
928
-				$this->correctFolderSize($parent);
929
-			}
930
-		}
931
-	}
932
-
933
-	/**
934
-	 * get the incomplete count that shares parent $folder
935
-	 *
936
-	 * @param int $fileId the file id of the folder
937
-	 * @return int
938
-	 */
939
-	public function getIncompleteChildrenCount($fileId) {
940
-		if ($fileId > -1) {
941
-			$query = $this->getQueryBuilder();
942
-			$query->select($query->func()->count())
943
-				->from('filecache')
944
-				->whereParent($fileId)
945
-				->whereStorageId($this->getNumericStorageId())
946
-				->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)));
947
-
948
-			$result = $query->executeQuery();
949
-			$size = (int)$result->fetchOne();
950
-			$result->closeCursor();
951
-
952
-			return $size;
953
-		}
954
-		return -1;
955
-	}
956
-
957
-	/**
958
-	 * calculate the size of a folder and set it in the cache
959
-	 *
960
-	 * @param string $path
961
-	 * @param array|null|ICacheEntry $entry (optional) meta data of the folder
962
-	 * @return int|float
963
-	 */
964
-	public function calculateFolderSize($path, $entry = null) {
965
-		return $this->calculateFolderSizeInner($path, $entry);
966
-	}
967
-
968
-
969
-	/**
970
-	 * inner function because we can't add new params to the public function without breaking any child classes
971
-	 *
972
-	 * @param string $path
973
-	 * @param array|null|ICacheEntry $entry (optional) meta data of the folder
974
-	 * @param bool $ignoreUnknown don't mark the folder size as unknown if any of it's children are unknown
975
-	 * @return int|float
976
-	 */
977
-	protected function calculateFolderSizeInner(string $path, $entry = null, bool $ignoreUnknown = false) {
978
-		$totalSize = 0;
979
-		if (is_null($entry) || !isset($entry['fileid'])) {
980
-			$entry = $this->get($path);
981
-		}
982
-		if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
983
-			$id = $entry['fileid'];
984
-
985
-			$query = $this->getQueryBuilder();
986
-			$query->select('size', 'unencrypted_size')
987
-				->from('filecache')
988
-				->whereStorageId($this->getNumericStorageId())
989
-				->whereParent($id);
990
-			if ($ignoreUnknown) {
991
-				$query->andWhere($query->expr()->gte('size', $query->createNamedParameter(0)));
992
-			}
993
-
994
-			$result = $query->executeQuery();
995
-			$rows = $result->fetchAll();
996
-			$result->closeCursor();
997
-
998
-			if ($rows) {
999
-				$sizes = array_map(function (array $row) {
1000
-					return Util::numericToNumber($row['size']);
1001
-				}, $rows);
1002
-				$unencryptedOnlySizes = array_map(function (array $row) {
1003
-					return Util::numericToNumber($row['unencrypted_size']);
1004
-				}, $rows);
1005
-				$unencryptedSizes = array_map(function (array $row) {
1006
-					return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
1007
-				}, $rows);
1008
-
1009
-				$sum = array_sum($sizes);
1010
-				$min = min($sizes);
1011
-
1012
-				$unencryptedSum = array_sum($unencryptedSizes);
1013
-				$unencryptedMin = min($unencryptedSizes);
1014
-				$unencryptedMax = max($unencryptedOnlySizes);
1015
-
1016
-				$sum = 0 + $sum;
1017
-				$min = 0 + $min;
1018
-				if ($min === -1) {
1019
-					$totalSize = $min;
1020
-				} else {
1021
-					$totalSize = $sum;
1022
-				}
1023
-				if ($unencryptedMin === -1 || $min === -1) {
1024
-					$unencryptedTotal = $unencryptedMin;
1025
-				} else {
1026
-					$unencryptedTotal = $unencryptedSum;
1027
-				}
1028
-			} else {
1029
-				$totalSize = 0;
1030
-				$unencryptedTotal = 0;
1031
-				$unencryptedMax = 0;
1032
-			}
1033
-
1034
-			// only set unencrypted size for a folder if any child entries have it set, or the folder is empty
1035
-			$shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || ($entry['unencrypted_size'] ?? 0) > 0;
1036
-			if ($entry['size'] !== $totalSize || (($entry['unencrypted_size'] ?? 0) !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) {
1037
-				if ($shouldWriteUnEncryptedSize) {
1038
-					// if all children have an unencrypted size of 0, just set the folder unencrypted size to 0 instead of summing the sizes
1039
-					if ($unencryptedMax === 0) {
1040
-						$unencryptedTotal = 0;
1041
-					}
1042
-
1043
-					$this->update($id, [
1044
-						'size' => $totalSize,
1045
-						'unencrypted_size' => $unencryptedTotal,
1046
-					]);
1047
-				} else {
1048
-					$this->update($id, [
1049
-						'size' => $totalSize,
1050
-					]);
1051
-				}
1052
-			}
1053
-		}
1054
-		return $totalSize;
1055
-	}
1056
-
1057
-	/**
1058
-	 * get all file ids on the files on the storage
1059
-	 *
1060
-	 * @return int[]
1061
-	 */
1062
-	public function getAll() {
1063
-		$query = $this->getQueryBuilder();
1064
-		$query->select('fileid')
1065
-			->from('filecache')
1066
-			->whereStorageId($this->getNumericStorageId());
1067
-
1068
-		$result = $query->executeQuery();
1069
-		$files = $result->fetchAll(\PDO::FETCH_COLUMN);
1070
-		$result->closeCursor();
1071
-
1072
-		return array_map(function ($id) {
1073
-			return (int)$id;
1074
-		}, $files);
1075
-	}
1076
-
1077
-	/**
1078
-	 * find a folder in the cache which has not been fully scanned
1079
-	 *
1080
-	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
1081
-	 * use the one with the highest id gives the best result with the background scanner, since that is most
1082
-	 * likely the folder where we stopped scanning previously
1083
-	 *
1084
-	 * @return string|false the path of the folder or false when no folder matched
1085
-	 */
1086
-	public function getIncomplete() {
1087
-		$query = $this->getQueryBuilder();
1088
-		$query->select('path')
1089
-			->from('filecache')
1090
-			->whereStorageId($this->getNumericStorageId())
1091
-			->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
1092
-			->orderBy('fileid', 'DESC')
1093
-			->setMaxResults(1);
1094
-
1095
-		$result = $query->executeQuery();
1096
-		$path = $result->fetchOne();
1097
-		$result->closeCursor();
1098
-
1099
-		return $path === false ? false : (string)$path;
1100
-	}
1101
-
1102
-	/**
1103
-	 * get the path of a file on this storage by it's file id
1104
-	 *
1105
-	 * @param int $id the file id of the file or folder to search
1106
-	 * @return string|null the path of the file (relative to the storage) or null if a file with the given id does not exists within this cache
1107
-	 */
1108
-	public function getPathById($id) {
1109
-		$query = $this->getQueryBuilder();
1110
-		$query->select('path')
1111
-			->from('filecache')
1112
-			->whereStorageId($this->getNumericStorageId())
1113
-			->whereFileId($id);
1114
-
1115
-		$result = $query->executeQuery();
1116
-		$path = $result->fetchOne();
1117
-		$result->closeCursor();
1118
-
1119
-		if ($path === false) {
1120
-			return null;
1121
-		}
1122
-
1123
-		return (string)$path;
1124
-	}
1125
-
1126
-	/**
1127
-	 * get the storage id of the storage for a file and the internal path of the file
1128
-	 * unlike getPathById this does not limit the search to files on this storage and
1129
-	 * instead does a global search in the cache table
1130
-	 *
1131
-	 * @param int $id
1132
-	 * @return array first element holding the storage id, second the path
1133
-	 * @deprecated 17.0.0 use getPathById() instead
1134
-	 */
1135
-	public static function getById($id) {
1136
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
1137
-		$query->select('path', 'storage')
1138
-			->from('filecache')
1139
-			->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
1140
-
1141
-		$result = $query->executeQuery();
1142
-		$row = $result->fetch();
1143
-		$result->closeCursor();
1144
-
1145
-		if ($row) {
1146
-			$numericId = $row['storage'];
1147
-			$path = $row['path'];
1148
-		} else {
1149
-			return null;
1150
-		}
1151
-
1152
-		if ($id = Storage::getStorageId($numericId)) {
1153
-			return [$id, $path];
1154
-		} else {
1155
-			return null;
1156
-		}
1157
-	}
1158
-
1159
-	/**
1160
-	 * normalize the given path
1161
-	 *
1162
-	 * @param string $path
1163
-	 * @return string
1164
-	 */
1165
-	public function normalize($path) {
1166
-		return trim(\OC_Util::normalizeUnicode($path), '/');
1167
-	}
1168
-
1169
-	/**
1170
-	 * Copy a file or folder in the cache
1171
-	 *
1172
-	 * @param ICache $sourceCache
1173
-	 * @param ICacheEntry $sourceEntry
1174
-	 * @param string $targetPath
1175
-	 * @return int fileId of copied entry
1176
-	 */
1177
-	public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1178
-		if ($sourceEntry->getId() < 0) {
1179
-			throw new \RuntimeException('Invalid source cache entry on copyFromCache');
1180
-		}
1181
-		$data = $this->cacheEntryToArray($sourceEntry);
1182
-
1183
-		// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1184
-		if ($sourceCache instanceof Cache
1185
-			&& $sourceCache->hasEncryptionWrapper()
1186
-			&& !$this->shouldEncrypt($targetPath)) {
1187
-			$data['encrypted'] = 0;
1188
-		}
1189
-
1190
-		$fileId = $this->put($targetPath, $data);
1191
-		if ($fileId <= 0) {
1192
-			throw new \RuntimeException('Failed to copy to ' . $targetPath . ' from cache with source data ' . json_encode($data) . ' ');
1193
-		}
1194
-		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1195
-			$folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1196
-			foreach ($folderContent as $subEntry) {
1197
-				$subTargetPath = $targetPath . '/' . $subEntry->getName();
1198
-				$this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1199
-			}
1200
-		}
1201
-		return $fileId;
1202
-	}
1203
-
1204
-	private function cacheEntryToArray(ICacheEntry $entry): array {
1205
-		$data = [
1206
-			'size' => $entry->getSize(),
1207
-			'mtime' => $entry->getMTime(),
1208
-			'storage_mtime' => $entry->getStorageMTime(),
1209
-			'mimetype' => $entry->getMimeType(),
1210
-			'mimepart' => $entry->getMimePart(),
1211
-			'etag' => $entry->getEtag(),
1212
-			'permissions' => $entry->getPermissions(),
1213
-			'encrypted' => $entry->isEncrypted(),
1214
-			'creation_time' => $entry->getCreationTime(),
1215
-			'upload_time' => $entry->getUploadTime(),
1216
-			'metadata_etag' => $entry->getMetadataEtag(),
1217
-		];
1218
-		if ($entry instanceof CacheEntry && isset($entry['scan_permissions'])) {
1219
-			$data['permissions'] = $entry['scan_permissions'];
1220
-		}
1221
-		return $data;
1222
-	}
1223
-
1224
-	public function getQueryFilterForStorage(): ISearchOperator {
1225
-		return new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $this->getNumericStorageId());
1226
-	}
1227
-
1228
-	public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
1229
-		if ($rawEntry->getStorageId() === $this->getNumericStorageId()) {
1230
-			return $rawEntry;
1231
-		} else {
1232
-			return null;
1233
-		}
1234
-	}
1235
-
1236
-	private function moveFromStorageSharded(ShardDefinition $shardDefinition, ICache $sourceCache, ICacheEntry $sourceEntry, $targetPath): void {
1237
-		$sourcePath = $sourceEntry->getPath();
1238
-		while ($sourceCache instanceof CacheWrapper) {
1239
-			if ($sourceCache instanceof CacheJail) {
1240
-				$sourcePath = $sourceCache->getSourcePath($sourcePath);
1241
-			}
1242
-			$sourceCache = $sourceCache->getCache();
1243
-		}
1244
-
1245
-		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1246
-			$fileIds = $this->getChildIds($sourceCache->getNumericStorageId(), $sourcePath);
1247
-		} else {
1248
-			$fileIds = [];
1249
-		}
1250
-		$fileIds[] = $sourceEntry->getId();
1251
-
1252
-		$helper = $this->connection->getCrossShardMoveHelper();
1253
-
1254
-		$sourceConnection = $helper->getConnection($shardDefinition, $sourceCache->getNumericStorageId());
1255
-		$targetConnection = $helper->getConnection($shardDefinition, $this->getNumericStorageId());
1256
-
1257
-		$cacheItems = $helper->loadItems($sourceConnection, 'filecache', 'fileid', $fileIds);
1258
-		$extendedItems = $helper->loadItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
1259
-		$metadataItems = $helper->loadItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
1260
-
1261
-		// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1262
-		$removeEncryptedFlag = ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper()) && !$this->hasEncryptionWrapper();
1263
-
1264
-		$sourcePathLength = strlen($sourcePath);
1265
-		foreach ($cacheItems as &$cacheItem) {
1266
-			if ($cacheItem['path'] === $sourcePath) {
1267
-				$cacheItem['path'] = $targetPath;
1268
-				$cacheItem['parent'] = $this->getParentId($targetPath);
1269
-				$cacheItem['name'] = basename($cacheItem['path']);
1270
-			} else {
1271
-				$cacheItem['path'] = $targetPath . '/' . substr($cacheItem['path'], $sourcePathLength + 1); // +1 for the leading slash
1272
-			}
1273
-			$cacheItem['path_hash'] = md5($cacheItem['path']);
1274
-			$cacheItem['storage'] = $this->getNumericStorageId();
1275
-			if ($removeEncryptedFlag) {
1276
-				$cacheItem['encrypted'] = 0;
1277
-			}
1278
-		}
1279
-
1280
-		$targetConnection->beginTransaction();
1281
-
1282
-		try {
1283
-			$helper->saveItems($targetConnection, 'filecache', $cacheItems);
1284
-			$helper->saveItems($targetConnection, 'filecache_extended', $extendedItems);
1285
-			$helper->saveItems($targetConnection, 'files_metadata', $metadataItems);
1286
-		} catch (\Exception $e) {
1287
-			$targetConnection->rollback();
1288
-			throw $e;
1289
-		}
1290
-
1291
-		$sourceConnection->beginTransaction();
1292
-
1293
-		try {
1294
-			$helper->deleteItems($sourceConnection, 'filecache', 'fileid', $fileIds);
1295
-			$helper->deleteItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
1296
-			$helper->deleteItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
1297
-		} catch (\Exception $e) {
1298
-			$targetConnection->rollback();
1299
-			$sourceConnection->rollBack();
1300
-			throw $e;
1301
-		}
1302
-
1303
-		try {
1304
-			$sourceConnection->commit();
1305
-		} catch (\Exception $e) {
1306
-			$targetConnection->rollback();
1307
-			throw $e;
1308
-		}
1309
-		$targetConnection->commit();
1310
-	}
50
+    use MoveFromCacheTrait {
51
+        MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
52
+    }
53
+
54
+    /**
55
+     * @var array partial data for the cache
56
+     */
57
+    protected array $partial = [];
58
+    protected string $storageId;
59
+    protected Storage $storageCache;
60
+    protected IMimeTypeLoader $mimetypeLoader;
61
+    protected IDBConnection $connection;
62
+    protected SystemConfig $systemConfig;
63
+    protected LoggerInterface $logger;
64
+    protected QuerySearchHelper $querySearchHelper;
65
+    protected IEventDispatcher $eventDispatcher;
66
+    protected IFilesMetadataManager $metadataManager;
67
+
68
+    public function __construct(
69
+        private IStorage $storage,
70
+        // this constructor is used in to many pleases to easily do proper di
71
+        // so instead we group it all together
72
+        ?CacheDependencies $dependencies = null,
73
+    ) {
74
+        $this->storageId = $storage->getId();
75
+        if (strlen($this->storageId) > 64) {
76
+            $this->storageId = md5($this->storageId);
77
+        }
78
+        if (!$dependencies) {
79
+            $dependencies = \OCP\Server::get(CacheDependencies::class);
80
+        }
81
+        $this->storageCache = new Storage($this->storage, true, $dependencies->getConnection());
82
+        $this->mimetypeLoader = $dependencies->getMimeTypeLoader();
83
+        $this->connection = $dependencies->getConnection();
84
+        $this->systemConfig = $dependencies->getSystemConfig();
85
+        $this->logger = $dependencies->getLogger();
86
+        $this->querySearchHelper = $dependencies->getQuerySearchHelper();
87
+        $this->eventDispatcher = $dependencies->getEventDispatcher();
88
+        $this->metadataManager = $dependencies->getMetadataManager();
89
+    }
90
+
91
+    protected function getQueryBuilder() {
92
+        return new CacheQueryBuilder(
93
+            $this->connection->getQueryBuilder(),
94
+            $this->metadataManager,
95
+        );
96
+    }
97
+
98
+    public function getStorageCache(): Storage {
99
+        return $this->storageCache;
100
+    }
101
+
102
+    /**
103
+     * Get the numeric storage id for this cache's storage
104
+     *
105
+     * @return int
106
+     */
107
+    public function getNumericStorageId() {
108
+        return $this->storageCache->getNumericId();
109
+    }
110
+
111
+    /**
112
+     * get the stored metadata of a file or folder
113
+     *
114
+     * @param string|int $file either the path of a file or folder or the file id for a file or folder
115
+     * @return ICacheEntry|false the cache entry as array or false if the file is not found in the cache
116
+     */
117
+    public function get($file) {
118
+        $query = $this->getQueryBuilder();
119
+        $query->selectFileCache();
120
+        $metadataQuery = $query->selectMetadata();
121
+
122
+        if (is_string($file) || $file == '') {
123
+            // normalize file
124
+            $file = $this->normalize($file);
125
+
126
+            $query->wherePath($file);
127
+        } else { //file id
128
+            $query->whereFileId($file);
129
+        }
130
+        $query->whereStorageId($this->getNumericStorageId());
131
+
132
+        $result = $query->executeQuery();
133
+        $data = $result->fetch();
134
+        $result->closeCursor();
135
+
136
+        if ($data !== false) {
137
+            $data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
138
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
139
+        } else {
140
+            //merge partial data
141
+            if (is_string($file) && isset($this->partial[$file])) {
142
+                return $this->partial[$file];
143
+            }
144
+        }
145
+
146
+        return false;
147
+    }
148
+
149
+    /**
150
+     * Create a CacheEntry from database row
151
+     *
152
+     * @param array $data
153
+     * @param IMimeTypeLoader $mimetypeLoader
154
+     * @return CacheEntry
155
+     */
156
+    public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
157
+        //fix types
158
+        $data['name'] = (string)$data['name'];
159
+        $data['path'] = (string)$data['path'];
160
+        $data['fileid'] = (int)$data['fileid'];
161
+        $data['parent'] = (int)$data['parent'];
162
+        $data['size'] = Util::numericToNumber($data['size']);
163
+        $data['unencrypted_size'] = Util::numericToNumber($data['unencrypted_size'] ?? 0);
164
+        $data['mtime'] = (int)$data['mtime'];
165
+        $data['storage_mtime'] = (int)$data['storage_mtime'];
166
+        $data['encryptedVersion'] = (int)$data['encrypted'];
167
+        $data['encrypted'] = (bool)$data['encrypted'];
168
+        $data['storage_id'] = $data['storage'];
169
+        $data['storage'] = (int)$data['storage'];
170
+        $data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
171
+        $data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
172
+        if ($data['storage_mtime'] == 0) {
173
+            $data['storage_mtime'] = $data['mtime'];
174
+        }
175
+        if (isset($data['f_permissions'])) {
176
+            $data['scan_permissions'] ??= $data['f_permissions'];
177
+        }
178
+        $data['permissions'] = (int)$data['permissions'];
179
+        if (isset($data['creation_time'])) {
180
+            $data['creation_time'] = (int)$data['creation_time'];
181
+        }
182
+        if (isset($data['upload_time'])) {
183
+            $data['upload_time'] = (int)$data['upload_time'];
184
+        }
185
+        return new CacheEntry($data);
186
+    }
187
+
188
+    /**
189
+     * get the metadata of all files stored in $folder
190
+     *
191
+     * @param string $folder
192
+     * @return ICacheEntry[]
193
+     */
194
+    public function getFolderContents($folder) {
195
+        $fileId = $this->getId($folder);
196
+        return $this->getFolderContentsById($fileId);
197
+    }
198
+
199
+    /**
200
+     * get the metadata of all files stored in $folder
201
+     *
202
+     * @param int $fileId the file id of the folder
203
+     * @return ICacheEntry[]
204
+     */
205
+    public function getFolderContentsById($fileId) {
206
+        if ($fileId > -1) {
207
+            $query = $this->getQueryBuilder();
208
+            $query->selectFileCache()
209
+                ->whereParent($fileId)
210
+                ->whereStorageId($this->getNumericStorageId())
211
+                ->orderBy('name', 'ASC');
212
+
213
+            $metadataQuery = $query->selectMetadata();
214
+
215
+            $result = $query->executeQuery();
216
+            $files = $result->fetchAll();
217
+            $result->closeCursor();
218
+
219
+            return array_map(function (array $data) use ($metadataQuery) {
220
+                $data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
221
+                return self::cacheEntryFromData($data, $this->mimetypeLoader);
222
+            }, $files);
223
+        }
224
+        return [];
225
+    }
226
+
227
+    /**
228
+     * insert or update meta data for a file or folder
229
+     *
230
+     * @param string $file
231
+     * @param array $data
232
+     *
233
+     * @return int file id
234
+     * @throws \RuntimeException
235
+     */
236
+    public function put($file, array $data) {
237
+        if (($id = $this->getId($file)) > -1) {
238
+            $this->update($id, $data);
239
+            return $id;
240
+        } else {
241
+            return $this->insert($file, $data);
242
+        }
243
+    }
244
+
245
+    /**
246
+     * insert meta data for a new file or folder
247
+     *
248
+     * @param string $file
249
+     * @param array $data
250
+     *
251
+     * @return int file id
252
+     * @throws \RuntimeException|Exception
253
+     */
254
+    public function insert($file, array $data) {
255
+        // normalize file
256
+        $file = $this->normalize($file);
257
+
258
+        if (isset($this->partial[$file])) { //add any saved partial data
259
+            $data = array_merge($this->partial[$file]->getData(), $data);
260
+            unset($this->partial[$file]);
261
+        }
262
+
263
+        $requiredFields = ['size', 'mtime', 'mimetype'];
264
+        foreach ($requiredFields as $field) {
265
+            if (!isset($data[$field])) { //data not complete save as partial and return
266
+                $this->partial[$file] = new CacheEntry($data);
267
+                return -1;
268
+            }
269
+        }
270
+
271
+        $data['path'] = $file;
272
+        if (!isset($data['parent'])) {
273
+            $data['parent'] = $this->getParentId($file);
274
+        }
275
+        if ($data['parent'] === -1 && $file !== '') {
276
+            throw new \Exception('Parent folder not in filecache for ' . $file);
277
+        }
278
+        $data['name'] = basename($file);
279
+
280
+        [$values, $extensionValues] = $this->normalizeData($data);
281
+        $storageId = $this->getNumericStorageId();
282
+        $values['storage'] = $storageId;
283
+
284
+        try {
285
+            $builder = $this->connection->getQueryBuilder();
286
+            $builder->insert('filecache');
287
+
288
+            foreach ($values as $column => $value) {
289
+                $builder->setValue($column, $builder->createNamedParameter($value));
290
+            }
291
+
292
+            if ($builder->executeStatement()) {
293
+                $fileId = $builder->getLastInsertId();
294
+
295
+                if (count($extensionValues)) {
296
+                    $query = $this->getQueryBuilder();
297
+                    $query->insert('filecache_extended');
298
+                    $query->hintShardKey('storage', $storageId);
299
+
300
+                    $query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
301
+                    foreach ($extensionValues as $column => $value) {
302
+                        $query->setValue($column, $query->createNamedParameter($value));
303
+                    }
304
+                    $query->executeStatement();
305
+                }
306
+
307
+                $event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
308
+                $this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
309
+                $this->eventDispatcher->dispatchTyped($event);
310
+                return $fileId;
311
+            }
312
+        } catch (Exception $e) {
313
+            if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
314
+                // entry exists already
315
+                if ($this->connection->inTransaction()) {
316
+                    $this->connection->commit();
317
+                    $this->connection->beginTransaction();
318
+                }
319
+            } else {
320
+                throw $e;
321
+            }
322
+        }
323
+
324
+        // The file was created in the meantime
325
+        if (($id = $this->getId($file)) > -1) {
326
+            $this->update($id, $data);
327
+            return $id;
328
+        } else {
329
+            throw new \RuntimeException('File entry could not be inserted but could also not be selected with getId() in order to perform an update. Please try again.');
330
+        }
331
+    }
332
+
333
+    /**
334
+     * update the metadata of an existing file or folder in the cache
335
+     *
336
+     * @param int $id the fileid of the existing file or folder
337
+     * @param array $data [$key => $value] the metadata to update, only the fields provided in the array will be updated, non-provided values will remain unchanged
338
+     */
339
+    public function update($id, array $data) {
340
+        if (isset($data['path'])) {
341
+            // normalize path
342
+            $data['path'] = $this->normalize($data['path']);
343
+        }
344
+
345
+        if (isset($data['name'])) {
346
+            // normalize path
347
+            $data['name'] = $this->normalize($data['name']);
348
+        }
349
+
350
+        [$values, $extensionValues] = $this->normalizeData($data);
351
+
352
+        if (count($values)) {
353
+            $query = $this->getQueryBuilder();
354
+
355
+            $query->update('filecache')
356
+                ->whereFileId($id)
357
+                ->whereStorageId($this->getNumericStorageId())
358
+                ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
359
+                    return $query->expr()->orX(
360
+                        $query->expr()->neq($key, $query->createNamedParameter($value)),
361
+                        $query->expr()->isNull($key)
362
+                    );
363
+                }, array_keys($values), array_values($values))));
364
+
365
+            foreach ($values as $key => $value) {
366
+                $query->set($key, $query->createNamedParameter($value));
367
+            }
368
+
369
+            $query->executeStatement();
370
+        }
371
+
372
+        if (count($extensionValues)) {
373
+            try {
374
+                $query = $this->getQueryBuilder();
375
+                $query->insert('filecache_extended');
376
+                $query->hintShardKey('storage', $this->getNumericStorageId());
377
+
378
+                $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
379
+                foreach ($extensionValues as $column => $value) {
380
+                    $query->setValue($column, $query->createNamedParameter($value));
381
+                }
382
+
383
+                $query->executeStatement();
384
+            } catch (Exception $e) {
385
+                if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
386
+                    throw $e;
387
+                }
388
+                $query = $this->getQueryBuilder();
389
+                $query->update('filecache_extended')
390
+                    ->whereFileId($id)
391
+                    ->hintShardKey('storage', $this->getNumericStorageId())
392
+                    ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
393
+                        return $query->expr()->orX(
394
+                            $query->expr()->neq($key, $query->createNamedParameter($value)),
395
+                            $query->expr()->isNull($key)
396
+                        );
397
+                    }, array_keys($extensionValues), array_values($extensionValues))));
398
+
399
+                foreach ($extensionValues as $key => $value) {
400
+                    $query->set($key, $query->createNamedParameter($value));
401
+                }
402
+
403
+                $query->executeStatement();
404
+            }
405
+        }
406
+
407
+        $path = $this->getPathById($id);
408
+        // path can still be null if the file doesn't exist
409
+        if ($path !== null) {
410
+            $event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
411
+            $this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
412
+            $this->eventDispatcher->dispatchTyped($event);
413
+        }
414
+    }
415
+
416
+    /**
417
+     * extract query parts and params array from data array
418
+     *
419
+     * @param array $data
420
+     * @return array
421
+     */
422
+    protected function normalizeData(array $data): array {
423
+        $fields = [
424
+            'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
425
+            'etag', 'permissions', 'checksum', 'storage', 'unencrypted_size'];
426
+        $extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
427
+
428
+        $doNotCopyStorageMTime = false;
429
+        if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
430
+            // this horrific magic tells it to not copy storage_mtime to mtime
431
+            unset($data['mtime']);
432
+            $doNotCopyStorageMTime = true;
433
+        }
434
+
435
+        $params = [];
436
+        $extensionParams = [];
437
+        foreach ($data as $name => $value) {
438
+            if (in_array($name, $fields)) {
439
+                if ($name === 'path') {
440
+                    $params['path_hash'] = md5($value);
441
+                } elseif ($name === 'mimetype') {
442
+                    $params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
443
+                    $value = $this->mimetypeLoader->getId($value);
444
+                } elseif ($name === 'storage_mtime') {
445
+                    if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
446
+                        $params['mtime'] = $value;
447
+                    }
448
+                } elseif ($name === 'encrypted') {
449
+                    if (isset($data['encryptedVersion'])) {
450
+                        $value = $data['encryptedVersion'];
451
+                    } else {
452
+                        // Boolean to integer conversion
453
+                        $value = $value ? 1 : 0;
454
+                    }
455
+                }
456
+                $params[$name] = $value;
457
+            }
458
+            if (in_array($name, $extensionFields)) {
459
+                $extensionParams[$name] = $value;
460
+            }
461
+        }
462
+        return [$params, array_filter($extensionParams)];
463
+    }
464
+
465
+    /**
466
+     * get the file id for a file
467
+     *
468
+     * A file id is a numeric id for a file or folder that's unique within an owncloud instance which stays the same for the lifetime of a file
469
+     *
470
+     * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
471
+     *
472
+     * @param string $file
473
+     * @return int
474
+     */
475
+    public function getId($file) {
476
+        // normalize file
477
+        $file = $this->normalize($file);
478
+
479
+        $query = $this->getQueryBuilder();
480
+        $query->select('fileid')
481
+            ->from('filecache')
482
+            ->whereStorageId($this->getNumericStorageId())
483
+            ->wherePath($file);
484
+
485
+        $result = $query->executeQuery();
486
+        $id = $result->fetchOne();
487
+        $result->closeCursor();
488
+
489
+        return $id === false ? -1 : (int)$id;
490
+    }
491
+
492
+    /**
493
+     * get the id of the parent folder of a file
494
+     *
495
+     * @param string $file
496
+     * @return int
497
+     */
498
+    public function getParentId($file) {
499
+        if ($file === '') {
500
+            return -1;
501
+        } else {
502
+            $parent = $this->getParentPath($file);
503
+            return (int)$this->getId($parent);
504
+        }
505
+    }
506
+
507
+    private function getParentPath($path) {
508
+        $parent = dirname($path);
509
+        if ($parent === '.') {
510
+            $parent = '';
511
+        }
512
+        return $parent;
513
+    }
514
+
515
+    /**
516
+     * check if a file is available in the cache
517
+     *
518
+     * @param string $file
519
+     * @return bool
520
+     */
521
+    public function inCache($file) {
522
+        return $this->getId($file) != -1;
523
+    }
524
+
525
+    /**
526
+     * remove a file or folder from the cache
527
+     *
528
+     * when removing a folder from the cache all files and folders inside the folder will be removed as well
529
+     *
530
+     * @param string $file
531
+     */
532
+    public function remove($file) {
533
+        $entry = $this->get($file);
534
+
535
+        if ($entry instanceof ICacheEntry) {
536
+            $query = $this->getQueryBuilder();
537
+            $query->delete('filecache')
538
+                ->whereStorageId($this->getNumericStorageId())
539
+                ->whereFileId($entry->getId());
540
+            $query->executeStatement();
541
+
542
+            $query = $this->getQueryBuilder();
543
+            $query->delete('filecache_extended')
544
+                ->whereFileId($entry->getId())
545
+                ->hintShardKey('storage', $this->getNumericStorageId());
546
+            $query->executeStatement();
547
+
548
+            if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
549
+                $this->removeChildren($entry);
550
+            }
551
+
552
+            $this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
553
+        }
554
+    }
555
+
556
+    /**
557
+     * Remove all children of a folder
558
+     *
559
+     * @param ICacheEntry $entry the cache entry of the folder to remove the children of
560
+     * @throws \OC\DatabaseException
561
+     */
562
+    private function removeChildren(ICacheEntry $entry) {
563
+        $parentIds = [$entry->getId()];
564
+        $queue = [$entry->getId()];
565
+        $deletedIds = [];
566
+        $deletedPaths = [];
567
+
568
+        // we walk depth first through the file tree, removing all filecache_extended attributes while we walk
569
+        // and collecting all folder ids to later use to delete the filecache entries
570
+        while ($entryId = array_pop($queue)) {
571
+            $children = $this->getFolderContentsById($entryId);
572
+            $childIds = array_map(function (ICacheEntry $cacheEntry) {
573
+                return $cacheEntry->getId();
574
+            }, $children);
575
+            $childPaths = array_map(function (ICacheEntry $cacheEntry) {
576
+                return $cacheEntry->getPath();
577
+            }, $children);
578
+
579
+            foreach ($childIds as $childId) {
580
+                $deletedIds[] = $childId;
581
+            }
582
+
583
+            foreach ($childPaths as $childPath) {
584
+                $deletedPaths[] = $childPath;
585
+            }
586
+
587
+            $query = $this->getQueryBuilder();
588
+            $query->delete('filecache_extended')
589
+                ->where($query->expr()->in('fileid', $query->createParameter('childIds')))
590
+                ->hintShardKey('storage', $this->getNumericStorageId());
591
+
592
+            foreach (array_chunk($childIds, 1000) as $childIdChunk) {
593
+                $query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
594
+                $query->executeStatement();
595
+            }
596
+
597
+            /** @var ICacheEntry[] $childFolders */
598
+            $childFolders = [];
599
+            foreach ($children as $child) {
600
+                if ($child->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
601
+                    $childFolders[] = $child;
602
+                }
603
+            }
604
+            foreach ($childFolders as $folder) {
605
+                $parentIds[] = $folder->getId();
606
+                $queue[] = $folder->getId();
607
+            }
608
+        }
609
+
610
+        $query = $this->getQueryBuilder();
611
+        $query->delete('filecache')
612
+            ->whereStorageId($this->getNumericStorageId())
613
+            ->whereParentInParameter('parentIds');
614
+
615
+        // Sorting before chunking allows the db to find the entries close to each
616
+        // other in the index
617
+        sort($parentIds, SORT_NUMERIC);
618
+        foreach (array_chunk($parentIds, 1000) as $parentIdChunk) {
619
+            $query->setParameter('parentIds', $parentIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
620
+            $query->executeStatement();
621
+        }
622
+
623
+        foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) {
624
+            $cacheEntryRemovedEvent = new CacheEntryRemovedEvent(
625
+                $this->storage,
626
+                $filePath,
627
+                $fileId,
628
+                $this->getNumericStorageId()
629
+            );
630
+            $this->eventDispatcher->dispatchTyped($cacheEntryRemovedEvent);
631
+        }
632
+    }
633
+
634
+    /**
635
+     * Move a file or folder in the cache
636
+     *
637
+     * @param string $source
638
+     * @param string $target
639
+     */
640
+    public function move($source, $target) {
641
+        $this->moveFromCache($this, $source, $target);
642
+    }
643
+
644
+    /**
645
+     * Get the storage id and path needed for a move
646
+     *
647
+     * @param string $path
648
+     * @return array [$storageId, $internalPath]
649
+     */
650
+    protected function getMoveInfo($path) {
651
+        return [$this->getNumericStorageId(), $path];
652
+    }
653
+
654
+    protected function hasEncryptionWrapper(): bool {
655
+        return $this->storage->instanceOfStorage(Encryption::class);
656
+    }
657
+
658
+    protected function shouldEncrypt(string $targetPath): bool {
659
+        if (!$this->storage->instanceOfStorage(Encryption::class)) {
660
+            return false;
661
+        }
662
+        return $this->storage->shouldEncrypt($targetPath);
663
+    }
664
+
665
+    /**
666
+     * Move a file or folder in the cache
667
+     *
668
+     * @param ICache $sourceCache
669
+     * @param string $sourcePath
670
+     * @param string $targetPath
671
+     * @throws \OC\DatabaseException
672
+     * @throws \Exception if the given storages have an invalid id
673
+     */
674
+    public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
675
+        if ($sourceCache instanceof Cache) {
676
+            // normalize source and target
677
+            $sourcePath = $this->normalize($sourcePath);
678
+            $targetPath = $this->normalize($targetPath);
679
+
680
+            $sourceData = $sourceCache->get($sourcePath);
681
+            if (!$sourceData) {
682
+                throw new \Exception('Source path not found in cache: ' . $sourcePath);
683
+            }
684
+
685
+            $shardDefinition = $this->connection->getShardDefinition('filecache');
686
+            if (
687
+                $shardDefinition
688
+                && $shardDefinition->getShardForKey($sourceCache->getNumericStorageId()) !== $shardDefinition->getShardForKey($this->getNumericStorageId())
689
+            ) {
690
+                $this->moveFromStorageSharded($shardDefinition, $sourceCache, $sourceData, $targetPath);
691
+                return;
692
+            }
693
+
694
+            $sourceId = $sourceData['fileid'];
695
+            $newParentId = $this->getParentId($targetPath);
696
+
697
+            [$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
698
+            [$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
699
+
700
+            if (is_null($sourceStorageId) || $sourceStorageId === false) {
701
+                throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
702
+            }
703
+            if (is_null($targetStorageId) || $targetStorageId === false) {
704
+                throw new \Exception('Invalid target storage id: ' . $targetStorageId);
705
+            }
706
+
707
+            if ($sourceData['mimetype'] === 'httpd/unix-directory') {
708
+                //update all child entries
709
+                $sourceLength = mb_strlen($sourcePath);
710
+
711
+                $childIds = $this->getChildIds($sourceStorageId, $sourcePath);
712
+
713
+                $childChunks = array_chunk($childIds, 1000);
714
+
715
+                $query = $this->getQueryBuilder();
716
+
717
+                $fun = $query->func();
718
+                $newPathFunction = $fun->concat(
719
+                    $query->createNamedParameter($targetPath),
720
+                    $fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
721
+                );
722
+                $query->update('filecache')
723
+                    ->set('path_hash', $fun->md5($newPathFunction))
724
+                    ->set('path', $newPathFunction)
725
+                    ->whereStorageId($sourceStorageId)
726
+                    ->andWhere($query->expr()->in('fileid', $query->createParameter('files')));
727
+
728
+                if ($sourceStorageId !== $targetStorageId) {
729
+                    $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
730
+                }
731
+
732
+                // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
733
+                if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
734
+                    $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
735
+                }
736
+
737
+                // Retry transaction in case of RetryableException like deadlocks.
738
+                // Retry up to 4 times because we should receive up to 4 concurrent requests from the frontend
739
+                $retryLimit = 4;
740
+                for ($i = 1; $i <= $retryLimit; $i++) {
741
+                    try {
742
+                        $this->connection->beginTransaction();
743
+                        foreach ($childChunks as $chunk) {
744
+                            $query->setParameter('files', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
745
+                            $query->executeStatement();
746
+                        }
747
+                        break;
748
+                    } catch (\OC\DatabaseException $e) {
749
+                        $this->connection->rollBack();
750
+                        throw $e;
751
+                    } catch (DbalException $e) {
752
+                        $this->connection->rollBack();
753
+
754
+                        if (!$e->isRetryable()) {
755
+                            throw $e;
756
+                        }
757
+
758
+                        // Simply throw if we already retried 4 times.
759
+                        if ($i === $retryLimit) {
760
+                            throw $e;
761
+                        }
762
+
763
+                        // Sleep a bit to give some time to the other transaction to finish.
764
+                        usleep(100 * 1000 * $i);
765
+                    }
766
+                }
767
+            } else {
768
+                $this->connection->beginTransaction();
769
+            }
770
+
771
+            $query = $this->getQueryBuilder();
772
+            $query->update('filecache')
773
+                ->set('path', $query->createNamedParameter($targetPath))
774
+                ->set('path_hash', $query->createNamedParameter(md5($targetPath)))
775
+                ->set('name', $query->createNamedParameter(basename($targetPath)))
776
+                ->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
777
+                ->whereStorageId($sourceStorageId)
778
+                ->whereFileId($sourceId);
779
+
780
+            if ($sourceStorageId !== $targetStorageId) {
781
+                $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
782
+            }
783
+
784
+            // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
785
+            if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
786
+                $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
787
+            }
788
+
789
+            $query->executeStatement();
790
+
791
+            $this->connection->commit();
792
+
793
+            if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
794
+                \OCP\Server::get(\OCP\Files\Config\IUserMountCache::class)->clear();
795
+                $this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
796
+                $event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
797
+                $this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
798
+                $this->eventDispatcher->dispatchTyped($event);
799
+            } else {
800
+                $event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
801
+                $this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
802
+                $this->eventDispatcher->dispatchTyped($event);
803
+            }
804
+        } else {
805
+            $this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
806
+        }
807
+    }
808
+
809
+    private function getChildIds(int $storageId, string $path): array {
810
+        $query = $this->connection->getQueryBuilder();
811
+        $query->select('fileid')
812
+            ->from('filecache')
813
+            ->where($query->expr()->eq('storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
814
+            ->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($path) . '/%')));
815
+        return $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
816
+    }
817
+
818
+    /**
819
+     * remove all entries for files that are stored on the storage from the cache
820
+     */
821
+    public function clear() {
822
+        $query = $this->getQueryBuilder();
823
+        $query->delete('filecache')
824
+            ->whereStorageId($this->getNumericStorageId());
825
+        $query->executeStatement();
826
+
827
+        $query = $this->connection->getQueryBuilder();
828
+        $query->delete('storages')
829
+            ->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
830
+        $query->executeStatement();
831
+    }
832
+
833
+    /**
834
+     * Get the scan status of a file
835
+     *
836
+     * - Cache::NOT_FOUND: File is not in the cache
837
+     * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
838
+     * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
839
+     * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
840
+     *
841
+     * @param string $file
842
+     *
843
+     * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
844
+     */
845
+    public function getStatus($file) {
846
+        // normalize file
847
+        $file = $this->normalize($file);
848
+
849
+        $query = $this->getQueryBuilder();
850
+        $query->select('size')
851
+            ->from('filecache')
852
+            ->whereStorageId($this->getNumericStorageId())
853
+            ->wherePath($file);
854
+
855
+        $result = $query->executeQuery();
856
+        $size = $result->fetchOne();
857
+        $result->closeCursor();
858
+
859
+        if ($size !== false) {
860
+            if ((int)$size === -1) {
861
+                return self::SHALLOW;
862
+            } else {
863
+                return self::COMPLETE;
864
+            }
865
+        } else {
866
+            if (isset($this->partial[$file])) {
867
+                return self::PARTIAL;
868
+            } else {
869
+                return self::NOT_FOUND;
870
+            }
871
+        }
872
+    }
873
+
874
+    /**
875
+     * search for files matching $pattern
876
+     *
877
+     * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
878
+     * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
879
+     */
880
+    public function search($pattern) {
881
+        $operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', $pattern);
882
+        return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
883
+    }
884
+
885
+    /**
886
+     * search for files by mimetype
887
+     *
888
+     * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
889
+     *                         where it will search for all mimetypes in the group ('image/*')
890
+     * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
891
+     */
892
+    public function searchByMime($mimetype) {
893
+        if (!str_contains($mimetype, '/')) {
894
+            $operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
895
+        } else {
896
+            $operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
897
+        }
898
+        return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
899
+    }
900
+
901
+    public function searchQuery(ISearchQuery $query) {
902
+        return current($this->querySearchHelper->searchInCaches($query, [$this]));
903
+    }
904
+
905
+    /**
906
+     * Re-calculate the folder size and the size of all parent folders
907
+     *
908
+     * @param array|ICacheEntry|null $data (optional) meta data of the folder
909
+     */
910
+    public function correctFolderSize(string $path, $data = null, bool $isBackgroundScan = false): void {
911
+        $this->calculateFolderSize($path, $data);
912
+
913
+        if ($path !== '') {
914
+            $parent = dirname($path);
915
+            if ($parent === '.' || $parent === '/') {
916
+                $parent = '';
917
+            }
918
+
919
+            if ($isBackgroundScan) {
920
+                $parentData = $this->get($parent);
921
+                if ($parentData !== false
922
+                    && $parentData['size'] !== -1
923
+                    && $this->getIncompleteChildrenCount($parentData['fileid']) === 0
924
+                ) {
925
+                    $this->correctFolderSize($parent, $parentData, $isBackgroundScan);
926
+                }
927
+            } else {
928
+                $this->correctFolderSize($parent);
929
+            }
930
+        }
931
+    }
932
+
933
+    /**
934
+     * get the incomplete count that shares parent $folder
935
+     *
936
+     * @param int $fileId the file id of the folder
937
+     * @return int
938
+     */
939
+    public function getIncompleteChildrenCount($fileId) {
940
+        if ($fileId > -1) {
941
+            $query = $this->getQueryBuilder();
942
+            $query->select($query->func()->count())
943
+                ->from('filecache')
944
+                ->whereParent($fileId)
945
+                ->whereStorageId($this->getNumericStorageId())
946
+                ->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)));
947
+
948
+            $result = $query->executeQuery();
949
+            $size = (int)$result->fetchOne();
950
+            $result->closeCursor();
951
+
952
+            return $size;
953
+        }
954
+        return -1;
955
+    }
956
+
957
+    /**
958
+     * calculate the size of a folder and set it in the cache
959
+     *
960
+     * @param string $path
961
+     * @param array|null|ICacheEntry $entry (optional) meta data of the folder
962
+     * @return int|float
963
+     */
964
+    public function calculateFolderSize($path, $entry = null) {
965
+        return $this->calculateFolderSizeInner($path, $entry);
966
+    }
967
+
968
+
969
+    /**
970
+     * inner function because we can't add new params to the public function without breaking any child classes
971
+     *
972
+     * @param string $path
973
+     * @param array|null|ICacheEntry $entry (optional) meta data of the folder
974
+     * @param bool $ignoreUnknown don't mark the folder size as unknown if any of it's children are unknown
975
+     * @return int|float
976
+     */
977
+    protected function calculateFolderSizeInner(string $path, $entry = null, bool $ignoreUnknown = false) {
978
+        $totalSize = 0;
979
+        if (is_null($entry) || !isset($entry['fileid'])) {
980
+            $entry = $this->get($path);
981
+        }
982
+        if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
983
+            $id = $entry['fileid'];
984
+
985
+            $query = $this->getQueryBuilder();
986
+            $query->select('size', 'unencrypted_size')
987
+                ->from('filecache')
988
+                ->whereStorageId($this->getNumericStorageId())
989
+                ->whereParent($id);
990
+            if ($ignoreUnknown) {
991
+                $query->andWhere($query->expr()->gte('size', $query->createNamedParameter(0)));
992
+            }
993
+
994
+            $result = $query->executeQuery();
995
+            $rows = $result->fetchAll();
996
+            $result->closeCursor();
997
+
998
+            if ($rows) {
999
+                $sizes = array_map(function (array $row) {
1000
+                    return Util::numericToNumber($row['size']);
1001
+                }, $rows);
1002
+                $unencryptedOnlySizes = array_map(function (array $row) {
1003
+                    return Util::numericToNumber($row['unencrypted_size']);
1004
+                }, $rows);
1005
+                $unencryptedSizes = array_map(function (array $row) {
1006
+                    return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
1007
+                }, $rows);
1008
+
1009
+                $sum = array_sum($sizes);
1010
+                $min = min($sizes);
1011
+
1012
+                $unencryptedSum = array_sum($unencryptedSizes);
1013
+                $unencryptedMin = min($unencryptedSizes);
1014
+                $unencryptedMax = max($unencryptedOnlySizes);
1015
+
1016
+                $sum = 0 + $sum;
1017
+                $min = 0 + $min;
1018
+                if ($min === -1) {
1019
+                    $totalSize = $min;
1020
+                } else {
1021
+                    $totalSize = $sum;
1022
+                }
1023
+                if ($unencryptedMin === -1 || $min === -1) {
1024
+                    $unencryptedTotal = $unencryptedMin;
1025
+                } else {
1026
+                    $unencryptedTotal = $unencryptedSum;
1027
+                }
1028
+            } else {
1029
+                $totalSize = 0;
1030
+                $unencryptedTotal = 0;
1031
+                $unencryptedMax = 0;
1032
+            }
1033
+
1034
+            // only set unencrypted size for a folder if any child entries have it set, or the folder is empty
1035
+            $shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || ($entry['unencrypted_size'] ?? 0) > 0;
1036
+            if ($entry['size'] !== $totalSize || (($entry['unencrypted_size'] ?? 0) !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) {
1037
+                if ($shouldWriteUnEncryptedSize) {
1038
+                    // if all children have an unencrypted size of 0, just set the folder unencrypted size to 0 instead of summing the sizes
1039
+                    if ($unencryptedMax === 0) {
1040
+                        $unencryptedTotal = 0;
1041
+                    }
1042
+
1043
+                    $this->update($id, [
1044
+                        'size' => $totalSize,
1045
+                        'unencrypted_size' => $unencryptedTotal,
1046
+                    ]);
1047
+                } else {
1048
+                    $this->update($id, [
1049
+                        'size' => $totalSize,
1050
+                    ]);
1051
+                }
1052
+            }
1053
+        }
1054
+        return $totalSize;
1055
+    }
1056
+
1057
+    /**
1058
+     * get all file ids on the files on the storage
1059
+     *
1060
+     * @return int[]
1061
+     */
1062
+    public function getAll() {
1063
+        $query = $this->getQueryBuilder();
1064
+        $query->select('fileid')
1065
+            ->from('filecache')
1066
+            ->whereStorageId($this->getNumericStorageId());
1067
+
1068
+        $result = $query->executeQuery();
1069
+        $files = $result->fetchAll(\PDO::FETCH_COLUMN);
1070
+        $result->closeCursor();
1071
+
1072
+        return array_map(function ($id) {
1073
+            return (int)$id;
1074
+        }, $files);
1075
+    }
1076
+
1077
+    /**
1078
+     * find a folder in the cache which has not been fully scanned
1079
+     *
1080
+     * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
1081
+     * use the one with the highest id gives the best result with the background scanner, since that is most
1082
+     * likely the folder where we stopped scanning previously
1083
+     *
1084
+     * @return string|false the path of the folder or false when no folder matched
1085
+     */
1086
+    public function getIncomplete() {
1087
+        $query = $this->getQueryBuilder();
1088
+        $query->select('path')
1089
+            ->from('filecache')
1090
+            ->whereStorageId($this->getNumericStorageId())
1091
+            ->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
1092
+            ->orderBy('fileid', 'DESC')
1093
+            ->setMaxResults(1);
1094
+
1095
+        $result = $query->executeQuery();
1096
+        $path = $result->fetchOne();
1097
+        $result->closeCursor();
1098
+
1099
+        return $path === false ? false : (string)$path;
1100
+    }
1101
+
1102
+    /**
1103
+     * get the path of a file on this storage by it's file id
1104
+     *
1105
+     * @param int $id the file id of the file or folder to search
1106
+     * @return string|null the path of the file (relative to the storage) or null if a file with the given id does not exists within this cache
1107
+     */
1108
+    public function getPathById($id) {
1109
+        $query = $this->getQueryBuilder();
1110
+        $query->select('path')
1111
+            ->from('filecache')
1112
+            ->whereStorageId($this->getNumericStorageId())
1113
+            ->whereFileId($id);
1114
+
1115
+        $result = $query->executeQuery();
1116
+        $path = $result->fetchOne();
1117
+        $result->closeCursor();
1118
+
1119
+        if ($path === false) {
1120
+            return null;
1121
+        }
1122
+
1123
+        return (string)$path;
1124
+    }
1125
+
1126
+    /**
1127
+     * get the storage id of the storage for a file and the internal path of the file
1128
+     * unlike getPathById this does not limit the search to files on this storage and
1129
+     * instead does a global search in the cache table
1130
+     *
1131
+     * @param int $id
1132
+     * @return array first element holding the storage id, second the path
1133
+     * @deprecated 17.0.0 use getPathById() instead
1134
+     */
1135
+    public static function getById($id) {
1136
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
1137
+        $query->select('path', 'storage')
1138
+            ->from('filecache')
1139
+            ->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
1140
+
1141
+        $result = $query->executeQuery();
1142
+        $row = $result->fetch();
1143
+        $result->closeCursor();
1144
+
1145
+        if ($row) {
1146
+            $numericId = $row['storage'];
1147
+            $path = $row['path'];
1148
+        } else {
1149
+            return null;
1150
+        }
1151
+
1152
+        if ($id = Storage::getStorageId($numericId)) {
1153
+            return [$id, $path];
1154
+        } else {
1155
+            return null;
1156
+        }
1157
+    }
1158
+
1159
+    /**
1160
+     * normalize the given path
1161
+     *
1162
+     * @param string $path
1163
+     * @return string
1164
+     */
1165
+    public function normalize($path) {
1166
+        return trim(\OC_Util::normalizeUnicode($path), '/');
1167
+    }
1168
+
1169
+    /**
1170
+     * Copy a file or folder in the cache
1171
+     *
1172
+     * @param ICache $sourceCache
1173
+     * @param ICacheEntry $sourceEntry
1174
+     * @param string $targetPath
1175
+     * @return int fileId of copied entry
1176
+     */
1177
+    public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1178
+        if ($sourceEntry->getId() < 0) {
1179
+            throw new \RuntimeException('Invalid source cache entry on copyFromCache');
1180
+        }
1181
+        $data = $this->cacheEntryToArray($sourceEntry);
1182
+
1183
+        // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1184
+        if ($sourceCache instanceof Cache
1185
+            && $sourceCache->hasEncryptionWrapper()
1186
+            && !$this->shouldEncrypt($targetPath)) {
1187
+            $data['encrypted'] = 0;
1188
+        }
1189
+
1190
+        $fileId = $this->put($targetPath, $data);
1191
+        if ($fileId <= 0) {
1192
+            throw new \RuntimeException('Failed to copy to ' . $targetPath . ' from cache with source data ' . json_encode($data) . ' ');
1193
+        }
1194
+        if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1195
+            $folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1196
+            foreach ($folderContent as $subEntry) {
1197
+                $subTargetPath = $targetPath . '/' . $subEntry->getName();
1198
+                $this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1199
+            }
1200
+        }
1201
+        return $fileId;
1202
+    }
1203
+
1204
+    private function cacheEntryToArray(ICacheEntry $entry): array {
1205
+        $data = [
1206
+            'size' => $entry->getSize(),
1207
+            'mtime' => $entry->getMTime(),
1208
+            'storage_mtime' => $entry->getStorageMTime(),
1209
+            'mimetype' => $entry->getMimeType(),
1210
+            'mimepart' => $entry->getMimePart(),
1211
+            'etag' => $entry->getEtag(),
1212
+            'permissions' => $entry->getPermissions(),
1213
+            'encrypted' => $entry->isEncrypted(),
1214
+            'creation_time' => $entry->getCreationTime(),
1215
+            'upload_time' => $entry->getUploadTime(),
1216
+            'metadata_etag' => $entry->getMetadataEtag(),
1217
+        ];
1218
+        if ($entry instanceof CacheEntry && isset($entry['scan_permissions'])) {
1219
+            $data['permissions'] = $entry['scan_permissions'];
1220
+        }
1221
+        return $data;
1222
+    }
1223
+
1224
+    public function getQueryFilterForStorage(): ISearchOperator {
1225
+        return new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $this->getNumericStorageId());
1226
+    }
1227
+
1228
+    public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
1229
+        if ($rawEntry->getStorageId() === $this->getNumericStorageId()) {
1230
+            return $rawEntry;
1231
+        } else {
1232
+            return null;
1233
+        }
1234
+    }
1235
+
1236
+    private function moveFromStorageSharded(ShardDefinition $shardDefinition, ICache $sourceCache, ICacheEntry $sourceEntry, $targetPath): void {
1237
+        $sourcePath = $sourceEntry->getPath();
1238
+        while ($sourceCache instanceof CacheWrapper) {
1239
+            if ($sourceCache instanceof CacheJail) {
1240
+                $sourcePath = $sourceCache->getSourcePath($sourcePath);
1241
+            }
1242
+            $sourceCache = $sourceCache->getCache();
1243
+        }
1244
+
1245
+        if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1246
+            $fileIds = $this->getChildIds($sourceCache->getNumericStorageId(), $sourcePath);
1247
+        } else {
1248
+            $fileIds = [];
1249
+        }
1250
+        $fileIds[] = $sourceEntry->getId();
1251
+
1252
+        $helper = $this->connection->getCrossShardMoveHelper();
1253
+
1254
+        $sourceConnection = $helper->getConnection($shardDefinition, $sourceCache->getNumericStorageId());
1255
+        $targetConnection = $helper->getConnection($shardDefinition, $this->getNumericStorageId());
1256
+
1257
+        $cacheItems = $helper->loadItems($sourceConnection, 'filecache', 'fileid', $fileIds);
1258
+        $extendedItems = $helper->loadItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
1259
+        $metadataItems = $helper->loadItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
1260
+
1261
+        // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1262
+        $removeEncryptedFlag = ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper()) && !$this->hasEncryptionWrapper();
1263
+
1264
+        $sourcePathLength = strlen($sourcePath);
1265
+        foreach ($cacheItems as &$cacheItem) {
1266
+            if ($cacheItem['path'] === $sourcePath) {
1267
+                $cacheItem['path'] = $targetPath;
1268
+                $cacheItem['parent'] = $this->getParentId($targetPath);
1269
+                $cacheItem['name'] = basename($cacheItem['path']);
1270
+            } else {
1271
+                $cacheItem['path'] = $targetPath . '/' . substr($cacheItem['path'], $sourcePathLength + 1); // +1 for the leading slash
1272
+            }
1273
+            $cacheItem['path_hash'] = md5($cacheItem['path']);
1274
+            $cacheItem['storage'] = $this->getNumericStorageId();
1275
+            if ($removeEncryptedFlag) {
1276
+                $cacheItem['encrypted'] = 0;
1277
+            }
1278
+        }
1279
+
1280
+        $targetConnection->beginTransaction();
1281
+
1282
+        try {
1283
+            $helper->saveItems($targetConnection, 'filecache', $cacheItems);
1284
+            $helper->saveItems($targetConnection, 'filecache_extended', $extendedItems);
1285
+            $helper->saveItems($targetConnection, 'files_metadata', $metadataItems);
1286
+        } catch (\Exception $e) {
1287
+            $targetConnection->rollback();
1288
+            throw $e;
1289
+        }
1290
+
1291
+        $sourceConnection->beginTransaction();
1292
+
1293
+        try {
1294
+            $helper->deleteItems($sourceConnection, 'filecache', 'fileid', $fileIds);
1295
+            $helper->deleteItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
1296
+            $helper->deleteItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
1297
+        } catch (\Exception $e) {
1298
+            $targetConnection->rollback();
1299
+            $sourceConnection->rollBack();
1300
+            throw $e;
1301
+        }
1302
+
1303
+        try {
1304
+            $sourceConnection->commit();
1305
+        } catch (\Exception $e) {
1306
+            $targetConnection->rollback();
1307
+            throw $e;
1308
+        }
1309
+        $targetConnection->commit();
1310
+    }
1311 1311
 }
Please login to merge, or discard this patch.