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