Passed
Push — master ( da6019...057c72 )
by John
15:30 queued 11s
created
lib/private/Files/Cache/Cache.php 2 patches
Indentation   +1060 added lines, -1060 removed lines patch added patch discarded remove patch
@@ -67,1064 +67,1064 @@
 block discarded – undo
67 67
  * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
68 68
  */
69 69
 class Cache implements ICache {
70
-	use MoveFromCacheTrait {
71
-		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
72
-	}
73
-
74
-	/**
75
-	 * @var array partial data for the cache
76
-	 */
77
-	protected $partial = [];
78
-
79
-	/**
80
-	 * @var string
81
-	 */
82
-	protected $storageId;
83
-
84
-	private $storage;
85
-
86
-	/**
87
-	 * @var Storage $storageCache
88
-	 */
89
-	protected $storageCache;
90
-
91
-	/** @var IMimeTypeLoader */
92
-	protected $mimetypeLoader;
93
-
94
-	/**
95
-	 * @var IDBConnection
96
-	 */
97
-	protected $connection;
98
-
99
-	/**
100
-	 * @var IEventDispatcher
101
-	 */
102
-	protected $eventDispatcher;
103
-
104
-	/** @var QuerySearchHelper */
105
-	protected $querySearchHelper;
106
-
107
-	/**
108
-	 * @param IStorage $storage
109
-	 */
110
-	public function __construct(IStorage $storage) {
111
-		$this->storageId = $storage->getId();
112
-		$this->storage = $storage;
113
-		if (strlen($this->storageId) > 64) {
114
-			$this->storageId = md5($this->storageId);
115
-		}
116
-
117
-		$this->storageCache = new Storage($storage);
118
-		$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
119
-		$this->connection = \OC::$server->getDatabaseConnection();
120
-		$this->eventDispatcher = \OC::$server->get(IEventDispatcher::class);
121
-		$this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
122
-	}
123
-
124
-	protected function getQueryBuilder() {
125
-		return new CacheQueryBuilder(
126
-			$this->connection,
127
-			\OC::$server->getSystemConfig(),
128
-			\OC::$server->getLogger(),
129
-			$this
130
-		);
131
-	}
132
-
133
-	/**
134
-	 * Get the numeric storage id for this cache's storage
135
-	 *
136
-	 * @return int
137
-	 */
138
-	public function getNumericStorageId() {
139
-		return $this->storageCache->getNumericId();
140
-	}
141
-
142
-	/**
143
-	 * get the stored metadata of a file or folder
144
-	 *
145
-	 * @param string | int $file either the path of a file or folder or the file id for a file or folder
146
-	 * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
147
-	 */
148
-	public function get($file) {
149
-		$query = $this->getQueryBuilder();
150
-		$query->selectFileCache();
151
-
152
-		if (is_string($file) or $file == '') {
153
-			// normalize file
154
-			$file = $this->normalize($file);
155
-
156
-			$query->whereStorageId()
157
-				->wherePath($file);
158
-		} else { //file id
159
-			$query->whereFileId($file);
160
-		}
161
-
162
-		$result = $query->execute();
163
-		$data = $result->fetch();
164
-		$result->closeCursor();
165
-
166
-		//merge partial data
167
-		if (!$data and is_string($file) and isset($this->partial[$file])) {
168
-			return $this->partial[$file];
169
-		} elseif (!$data) {
170
-			return $data;
171
-		} else {
172
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
173
-		}
174
-	}
175
-
176
-	/**
177
-	 * Create a CacheEntry from database row
178
-	 *
179
-	 * @param array $data
180
-	 * @param IMimeTypeLoader $mimetypeLoader
181
-	 * @return CacheEntry
182
-	 */
183
-	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
184
-		//fix types
185
-		$data['fileid'] = (int)$data['fileid'];
186
-		$data['parent'] = (int)$data['parent'];
187
-		$data['size'] = 0 + $data['size'];
188
-		$data['mtime'] = (int)$data['mtime'];
189
-		$data['storage_mtime'] = (int)$data['storage_mtime'];
190
-		$data['encryptedVersion'] = (int)$data['encrypted'];
191
-		$data['encrypted'] = (bool)$data['encrypted'];
192
-		$data['storage_id'] = $data['storage'];
193
-		$data['storage'] = (int)$data['storage'];
194
-		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
195
-		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
196
-		if ($data['storage_mtime'] == 0) {
197
-			$data['storage_mtime'] = $data['mtime'];
198
-		}
199
-		$data['permissions'] = (int)$data['permissions'];
200
-		if (isset($data['creation_time'])) {
201
-			$data['creation_time'] = (int)$data['creation_time'];
202
-		}
203
-		if (isset($data['upload_time'])) {
204
-			$data['upload_time'] = (int)$data['upload_time'];
205
-		}
206
-		return new CacheEntry($data);
207
-	}
208
-
209
-	/**
210
-	 * get the metadata of all files stored in $folder
211
-	 *
212
-	 * @param string $folder
213
-	 * @return ICacheEntry[]
214
-	 */
215
-	public function getFolderContents($folder) {
216
-		$fileId = $this->getId($folder);
217
-		return $this->getFolderContentsById($fileId);
218
-	}
219
-
220
-	/**
221
-	 * get the metadata of all files stored in $folder
222
-	 *
223
-	 * @param int $fileId the file id of the folder
224
-	 * @return ICacheEntry[]
225
-	 */
226
-	public function getFolderContentsById($fileId) {
227
-		if ($fileId > -1) {
228
-			$query = $this->getQueryBuilder();
229
-			$query->selectFileCache()
230
-				->whereParent($fileId)
231
-				->orderBy('name', 'ASC');
232
-
233
-			$result = $query->execute();
234
-			$files = $result->fetchAll();
235
-			$result->closeCursor();
236
-
237
-			return array_map(function (array $data) {
238
-				return self::cacheEntryFromData($data, $this->mimetypeLoader);
239
-			}, $files);
240
-		}
241
-		return [];
242
-	}
243
-
244
-	/**
245
-	 * insert or update meta data for a file or folder
246
-	 *
247
-	 * @param string $file
248
-	 * @param array $data
249
-	 *
250
-	 * @return int file id
251
-	 * @throws \RuntimeException
252
-	 */
253
-	public function put($file, array $data) {
254
-		if (($id = $this->getId($file)) > -1) {
255
-			$this->update($id, $data);
256
-			return $id;
257
-		} else {
258
-			return $this->insert($file, $data);
259
-		}
260
-	}
261
-
262
-	/**
263
-	 * insert meta data for a new file or folder
264
-	 *
265
-	 * @param string $file
266
-	 * @param array $data
267
-	 *
268
-	 * @return int file id
269
-	 * @throws \RuntimeException
270
-	 */
271
-	public function insert($file, array $data) {
272
-		// normalize file
273
-		$file = $this->normalize($file);
274
-
275
-		if (isset($this->partial[$file])) { //add any saved partial data
276
-			$data = array_merge($this->partial[$file], $data);
277
-			unset($this->partial[$file]);
278
-		}
279
-
280
-		$requiredFields = ['size', 'mtime', 'mimetype'];
281
-		foreach ($requiredFields as $field) {
282
-			if (!isset($data[$field])) { //data not complete save as partial and return
283
-				$this->partial[$file] = $data;
284
-				return -1;
285
-			}
286
-		}
287
-
288
-		$data['path'] = $file;
289
-		if (!isset($data['parent'])) {
290
-			$data['parent'] = $this->getParentId($file);
291
-		}
292
-		$data['name'] = basename($file);
293
-
294
-		[$values, $extensionValues] = $this->normalizeData($data);
295
-		$storageId = $this->getNumericStorageId();
296
-		$values['storage'] = $storageId;
297
-
298
-		try {
299
-			$builder = $this->connection->getQueryBuilder();
300
-			$builder->insert('filecache');
301
-
302
-			foreach ($values as $column => $value) {
303
-				$builder->setValue($column, $builder->createNamedParameter($value));
304
-			}
305
-
306
-			if ($builder->execute()) {
307
-				$fileId = $builder->getLastInsertId();
308
-
309
-				if (count($extensionValues)) {
310
-					$query = $this->getQueryBuilder();
311
-					$query->insert('filecache_extended');
312
-
313
-					$query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
314
-					foreach ($extensionValues as $column => $value) {
315
-						$query->setValue($column, $query->createNamedParameter($value));
316
-					}
317
-					$query->execute();
318
-				}
319
-
320
-				$event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
321
-				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
322
-				$this->eventDispatcher->dispatchTyped($event);
323
-				return $fileId;
324
-			}
325
-		} catch (UniqueConstraintViolationException $e) {
326
-			// entry exists already
327
-			if ($this->connection->inTransaction()) {
328
-				$this->connection->commit();
329
-				$this->connection->beginTransaction();
330
-			}
331
-		}
332
-
333
-		// The file was created in the mean time
334
-		if (($id = $this->getId($file)) > -1) {
335
-			$this->update($id, $data);
336
-			return $id;
337
-		} else {
338
-			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.');
339
-		}
340
-	}
341
-
342
-	/**
343
-	 * update the metadata of an existing file or folder in the cache
344
-	 *
345
-	 * @param int $id the fileid of the existing file or folder
346
-	 * @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
347
-	 */
348
-	public function update($id, array $data) {
349
-		if (isset($data['path'])) {
350
-			// normalize path
351
-			$data['path'] = $this->normalize($data['path']);
352
-		}
353
-
354
-		if (isset($data['name'])) {
355
-			// normalize path
356
-			$data['name'] = $this->normalize($data['name']);
357
-		}
358
-
359
-		[$values, $extensionValues] = $this->normalizeData($data);
360
-
361
-		if (count($values)) {
362
-			$query = $this->getQueryBuilder();
363
-
364
-			$query->update('filecache')
365
-				->whereFileId($id)
366
-				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
367
-					return $query->expr()->orX(
368
-						$query->expr()->neq($key, $query->createNamedParameter($value)),
369
-						$query->expr()->isNull($key)
370
-					);
371
-				}, array_keys($values), array_values($values))));
372
-
373
-			foreach ($values as $key => $value) {
374
-				$query->set($key, $query->createNamedParameter($value));
375
-			}
376
-
377
-			$query->execute();
378
-		}
379
-
380
-		if (count($extensionValues)) {
381
-			try {
382
-				$query = $this->getQueryBuilder();
383
-				$query->insert('filecache_extended');
384
-
385
-				$query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
386
-				foreach ($extensionValues as $column => $value) {
387
-					$query->setValue($column, $query->createNamedParameter($value));
388
-				}
389
-
390
-				$query->execute();
391
-			} catch (UniqueConstraintViolationException $e) {
392
-				$query = $this->getQueryBuilder();
393
-				$query->update('filecache_extended')
394
-					->whereFileId($id)
395
-					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
396
-						return $query->expr()->orX(
397
-							$query->expr()->neq($key, $query->createNamedParameter($value)),
398
-							$query->expr()->isNull($key)
399
-						);
400
-					}, array_keys($extensionValues), array_values($extensionValues))));
401
-
402
-				foreach ($extensionValues as $key => $value) {
403
-					$query->set($key, $query->createNamedParameter($value));
404
-				}
405
-
406
-				$query->execute();
407
-			}
408
-		}
409
-
410
-		$path = $this->getPathById($id);
411
-		// path can still be null if the file doesn't exist
412
-		if ($path !== null) {
413
-			$event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
414
-			$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
415
-			$this->eventDispatcher->dispatchTyped($event);
416
-		}
417
-	}
418
-
419
-	/**
420
-	 * extract query parts and params array from data array
421
-	 *
422
-	 * @param array $data
423
-	 * @return array
424
-	 */
425
-	protected function normalizeData(array $data): array {
426
-		$fields = [
427
-			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
428
-			'etag', 'permissions', 'checksum', 'storage'];
429
-		$extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
430
-
431
-		$doNotCopyStorageMTime = false;
432
-		if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
433
-			// this horrific magic tells it to not copy storage_mtime to mtime
434
-			unset($data['mtime']);
435
-			$doNotCopyStorageMTime = true;
436
-		}
437
-
438
-		$params = [];
439
-		$extensionParams = [];
440
-		foreach ($data as $name => $value) {
441
-			if (array_search($name, $fields) !== false) {
442
-				if ($name === 'path') {
443
-					$params['path_hash'] = md5($value);
444
-				} elseif ($name === 'mimetype') {
445
-					$params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
446
-					$value = $this->mimetypeLoader->getId($value);
447
-				} elseif ($name === 'storage_mtime') {
448
-					if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
449
-						$params['mtime'] = $value;
450
-					}
451
-				} elseif ($name === 'encrypted') {
452
-					if (isset($data['encryptedVersion'])) {
453
-						$value = $data['encryptedVersion'];
454
-					} else {
455
-						// Boolean to integer conversion
456
-						$value = $value ? 1 : 0;
457
-					}
458
-				}
459
-				$params[$name] = $value;
460
-			}
461
-			if (array_search($name, $extensionFields) !== false) {
462
-				$extensionParams[$name] = $value;
463
-			}
464
-		}
465
-		return [$params, array_filter($extensionParams)];
466
-	}
467
-
468
-	/**
469
-	 * get the file id for a file
470
-	 *
471
-	 * 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
472
-	 *
473
-	 * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
474
-	 *
475
-	 * @param string $file
476
-	 * @return int
477
-	 */
478
-	public function getId($file) {
479
-		// normalize file
480
-		$file = $this->normalize($file);
481
-
482
-		$query = $this->getQueryBuilder();
483
-		$query->select('fileid')
484
-			->from('filecache')
485
-			->whereStorageId()
486
-			->wherePath($file);
487
-
488
-		$result = $query->execute();
489
-		$id = $result->fetchOne();
490
-		$result->closeCursor();
491
-
492
-		return $id === false ? -1 : (int)$id;
493
-	}
494
-
495
-	/**
496
-	 * get the id of the parent folder of a file
497
-	 *
498
-	 * @param string $file
499
-	 * @return int
500
-	 */
501
-	public function getParentId($file) {
502
-		if ($file === '') {
503
-			return -1;
504
-		} else {
505
-			$parent = $this->getParentPath($file);
506
-			return (int)$this->getId($parent);
507
-		}
508
-	}
509
-
510
-	private function getParentPath($path) {
511
-		$parent = dirname($path);
512
-		if ($parent === '.') {
513
-			$parent = '';
514
-		}
515
-		return $parent;
516
-	}
517
-
518
-	/**
519
-	 * check if a file is available in the cache
520
-	 *
521
-	 * @param string $file
522
-	 * @return bool
523
-	 */
524
-	public function inCache($file) {
525
-		return $this->getId($file) != -1;
526
-	}
527
-
528
-	/**
529
-	 * remove a file or folder from the cache
530
-	 *
531
-	 * when removing a folder from the cache all files and folders inside the folder will be removed as well
532
-	 *
533
-	 * @param string $file
534
-	 */
535
-	public function remove($file) {
536
-		$entry = $this->get($file);
537
-
538
-		if ($entry) {
539
-			$query = $this->getQueryBuilder();
540
-			$query->delete('filecache')
541
-				->whereFileId($entry->getId());
542
-			$query->execute();
543
-
544
-			$query = $this->getQueryBuilder();
545
-			$query->delete('filecache_extended')
546
-				->whereFileId($entry->getId());
547
-			$query->execute();
548
-
549
-			if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
550
-				$this->removeChildren($entry);
551
-			}
552
-
553
-			$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
554
-		}
555
-	}
556
-
557
-	/**
558
-	 * Get all sub folders of a folder
559
-	 *
560
-	 * @param ICacheEntry $entry the cache entry of the folder to get the subfolders for
561
-	 * @return ICacheEntry[] the cache entries for the subfolders
562
-	 */
563
-	private function getSubFolders(ICacheEntry $entry) {
564
-		$children = $this->getFolderContentsById($entry->getId());
565
-		return array_filter($children, function ($child) {
566
-			return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
567
-		});
568
-	}
569
-
570
-	/**
571
-	 * Recursively remove all children of a folder
572
-	 *
573
-	 * @param ICacheEntry $entry the cache entry of the folder to remove the children of
574
-	 * @throws \OC\DatabaseException
575
-	 */
576
-	private function removeChildren(ICacheEntry $entry) {
577
-		$parentIds = [$entry->getId()];
578
-		$queue = [$entry->getId()];
579
-
580
-		// we walk depth first trough the file tree, removing all filecache_extended attributes while we walk
581
-		// and collecting all folder ids to later use to delete the filecache entries
582
-		while ($entryId = array_pop($queue)) {
583
-			$children = $this->getFolderContentsById($entryId);
584
-			$childIds = array_map(function (ICacheEntry $cacheEntry) {
585
-				return $cacheEntry->getId();
586
-			}, $children);
587
-
588
-			$query = $this->getQueryBuilder();
589
-			$query->delete('filecache_extended')
590
-				->where($query->expr()->in('fileid', $query->createNamedParameter($childIds, IQueryBuilder::PARAM_INT_ARRAY)));
591
-			$query->execute();
592
-
593
-			/** @var ICacheEntry[] $childFolders */
594
-			$childFolders = array_filter($children, function ($child) {
595
-				return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
596
-			});
597
-			foreach ($childFolders as $folder) {
598
-				$parentIds[] = $folder->getId();
599
-				$queue[] = $folder->getId();
600
-			}
601
-		}
602
-
603
-		$query = $this->getQueryBuilder();
604
-		$query->delete('filecache')
605
-			->whereParentIn($parentIds);
606
-		$query->execute();
607
-	}
608
-
609
-	/**
610
-	 * Move a file or folder in the cache
611
-	 *
612
-	 * @param string $source
613
-	 * @param string $target
614
-	 */
615
-	public function move($source, $target) {
616
-		$this->moveFromCache($this, $source, $target);
617
-	}
618
-
619
-	/**
620
-	 * Get the storage id and path needed for a move
621
-	 *
622
-	 * @param string $path
623
-	 * @return array [$storageId, $internalPath]
624
-	 */
625
-	protected function getMoveInfo($path) {
626
-		return [$this->getNumericStorageId(), $path];
627
-	}
628
-
629
-	/**
630
-	 * Move a file or folder in the cache
631
-	 *
632
-	 * @param ICache $sourceCache
633
-	 * @param string $sourcePath
634
-	 * @param string $targetPath
635
-	 * @throws \OC\DatabaseException
636
-	 * @throws \Exception if the given storages have an invalid id
637
-	 */
638
-	public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
639
-		if ($sourceCache instanceof Cache) {
640
-			// normalize source and target
641
-			$sourcePath = $this->normalize($sourcePath);
642
-			$targetPath = $this->normalize($targetPath);
643
-
644
-			$sourceData = $sourceCache->get($sourcePath);
645
-			if ($sourceData === false) {
646
-				throw new \Exception('Invalid source storage path: ' . $sourcePath);
647
-			}
648
-
649
-			$sourceId = $sourceData['fileid'];
650
-			$newParentId = $this->getParentId($targetPath);
651
-
652
-			[$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
653
-			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
654
-
655
-			if (is_null($sourceStorageId) || $sourceStorageId === false) {
656
-				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
657
-			}
658
-			if (is_null($targetStorageId) || $targetStorageId === false) {
659
-				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
660
-			}
661
-
662
-			$this->connection->beginTransaction();
663
-			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
664
-				//update all child entries
665
-				$sourceLength = mb_strlen($sourcePath);
666
-				$query = $this->connection->getQueryBuilder();
667
-
668
-				$fun = $query->func();
669
-				$newPathFunction = $fun->concat(
670
-					$query->createNamedParameter($targetPath),
671
-					$fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
672
-				);
673
-				$query->update('filecache')
674
-					->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
675
-					->set('path_hash', $fun->md5($newPathFunction))
676
-					->set('path', $newPathFunction)
677
-					->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
678
-					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
679
-
680
-				try {
681
-					$query->execute();
682
-				} catch (\OC\DatabaseException $e) {
683
-					$this->connection->rollBack();
684
-					throw $e;
685
-				}
686
-			}
687
-
688
-			$query = $this->getQueryBuilder();
689
-			$query->update('filecache')
690
-				->set('storage', $query->createNamedParameter($targetStorageId))
691
-				->set('path', $query->createNamedParameter($targetPath))
692
-				->set('path_hash', $query->createNamedParameter(md5($targetPath)))
693
-				->set('name', $query->createNamedParameter(basename($targetPath)))
694
-				->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
695
-				->whereFileId($sourceId);
696
-			$query->execute();
697
-
698
-			$this->connection->commit();
699
-
700
-			if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
701
-				$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
702
-				$event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
703
-				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
704
-				$this->eventDispatcher->dispatchTyped($event);
705
-			} else {
706
-				$event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
707
-				$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
708
-				$this->eventDispatcher->dispatchTyped($event);
709
-			}
710
-		} else {
711
-			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
712
-		}
713
-	}
714
-
715
-	/**
716
-	 * remove all entries for files that are stored on the storage from the cache
717
-	 */
718
-	public function clear() {
719
-		$query = $this->getQueryBuilder();
720
-		$query->delete('filecache')
721
-			->whereStorageId();
722
-		$query->execute();
723
-
724
-		$query = $this->connection->getQueryBuilder();
725
-		$query->delete('storages')
726
-			->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
727
-		$query->execute();
728
-	}
729
-
730
-	/**
731
-	 * Get the scan status of a file
732
-	 *
733
-	 * - Cache::NOT_FOUND: File is not in the cache
734
-	 * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
735
-	 * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
736
-	 * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
737
-	 *
738
-	 * @param string $file
739
-	 *
740
-	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
741
-	 */
742
-	public function getStatus($file) {
743
-		// normalize file
744
-		$file = $this->normalize($file);
745
-
746
-		$query = $this->getQueryBuilder();
747
-		$query->select('size')
748
-			->from('filecache')
749
-			->whereStorageId()
750
-			->wherePath($file);
751
-
752
-		$result = $query->execute();
753
-		$size = $result->fetchOne();
754
-		$result->closeCursor();
755
-
756
-		if ($size !== false) {
757
-			if ((int)$size === -1) {
758
-				return self::SHALLOW;
759
-			} else {
760
-				return self::COMPLETE;
761
-			}
762
-		} else {
763
-			if (isset($this->partial[$file])) {
764
-				return self::PARTIAL;
765
-			} else {
766
-				return self::NOT_FOUND;
767
-			}
768
-		}
769
-	}
770
-
771
-	/**
772
-	 * search for files matching $pattern
773
-	 *
774
-	 * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
775
-	 * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
776
-	 */
777
-	public function search($pattern) {
778
-		// normalize pattern
779
-		$pattern = $this->normalize($pattern);
780
-
781
-		if ($pattern === '%%') {
782
-			return [];
783
-		}
784
-
785
-		$query = $this->getQueryBuilder();
786
-		$query->selectFileCache()
787
-			->whereStorageId()
788
-			->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
789
-
790
-		$result = $query->execute();
791
-		$files = $result->fetchAll();
792
-		$result->closeCursor();
793
-
794
-		return array_map(function (array $data) {
795
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
796
-		}, $files);
797
-	}
798
-
799
-	/**
800
-	 * @param IResult $result
801
-	 * @return CacheEntry[]
802
-	 */
803
-	private function searchResultToCacheEntries(IResult $result): array {
804
-		$files = $result->fetchAll();
805
-
806
-		return array_map(function (array $data) {
807
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
808
-		}, $files);
809
-	}
810
-
811
-	/**
812
-	 * search for files by mimetype
813
-	 *
814
-	 * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
815
-	 *        where it will search for all mimetypes in the group ('image/*')
816
-	 * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
817
-	 */
818
-	public function searchByMime($mimetype) {
819
-		$mimeId = $this->mimetypeLoader->getId($mimetype);
820
-
821
-		$query = $this->getQueryBuilder();
822
-		$query->selectFileCache()
823
-			->whereStorageId();
824
-
825
-		if (strpos($mimetype, '/')) {
826
-			$query->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
827
-		} else {
828
-			$query->andWhere($query->expr()->eq('mimepart', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
829
-		}
830
-
831
-		$result = $query->execute();
832
-		$files = $result->fetchAll();
833
-		$result->closeCursor();
834
-
835
-		return array_map(function (array $data) {
836
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
837
-		}, $files);
838
-	}
839
-
840
-	public function searchQuery(ISearchQuery $searchQuery) {
841
-		$builder = $this->getQueryBuilder();
842
-
843
-		$query = $builder->selectFileCache('file');
844
-
845
-		$query->whereStorageId();
846
-
847
-		if ($this->querySearchHelper->shouldJoinTags($searchQuery->getSearchOperation())) {
848
-			$user = $searchQuery->getUser();
849
-			if ($user === null) {
850
-				throw new \InvalidArgumentException("Searching by tag requires the user to be set in the query");
851
-			}
852
-			$query
853
-				->innerJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid'))
854
-				->innerJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX(
855
-					$builder->expr()->eq('tagmap.type', 'tag.type'),
856
-					$builder->expr()->eq('tagmap.categoryid', 'tag.id')
857
-				))
858
-				->andWhere($builder->expr()->eq('tag.type', $builder->createNamedParameter('files')))
859
-				->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($user->getUID())));
860
-		}
861
-
862
-		$searchExpr = $this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation());
863
-		if ($searchExpr) {
864
-			$query->andWhere($searchExpr);
865
-		}
866
-
867
-		if ($searchQuery->limitToHome() && ($this instanceof HomeCache)) {
868
-			$query->andWhere($builder->expr()->like('path', $query->expr()->literal('files/%')));
869
-		}
870
-
871
-		$this->querySearchHelper->addSearchOrdersToQuery($query, $searchQuery->getOrder());
872
-
873
-		if ($searchQuery->getLimit()) {
874
-			$query->setMaxResults($searchQuery->getLimit());
875
-		}
876
-		if ($searchQuery->getOffset()) {
877
-			$query->setFirstResult($searchQuery->getOffset());
878
-		}
879
-
880
-		$result = $query->execute();
881
-		$cacheEntries = $this->searchResultToCacheEntries($result);
882
-		$result->closeCursor();
883
-		return $cacheEntries;
884
-	}
885
-
886
-	/**
887
-	 * Re-calculate the folder size and the size of all parent folders
888
-	 *
889
-	 * @param string|boolean $path
890
-	 * @param array $data (optional) meta data of the folder
891
-	 */
892
-	public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
893
-		$this->calculateFolderSize($path, $data);
894
-		if ($path !== '') {
895
-			$parent = dirname($path);
896
-			if ($parent === '.' or $parent === '/') {
897
-				$parent = '';
898
-			}
899
-			if ($isBackgroundScan) {
900
-				$parentData = $this->get($parent);
901
-				if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
902
-					$this->correctFolderSize($parent, $parentData, $isBackgroundScan);
903
-				}
904
-			} else {
905
-				$this->correctFolderSize($parent);
906
-			}
907
-		}
908
-	}
909
-
910
-	/**
911
-	 * get the incomplete count that shares parent $folder
912
-	 *
913
-	 * @param int $fileId the file id of the folder
914
-	 * @return int
915
-	 */
916
-	public function getIncompleteChildrenCount($fileId) {
917
-		if ($fileId > -1) {
918
-			$query = $this->getQueryBuilder();
919
-			$query->select($query->func()->count())
920
-				->from('filecache')
921
-				->whereParent($fileId)
922
-				->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
923
-
924
-			$result = $query->execute();
925
-			$size = (int)$result->fetchOne();
926
-			$result->closeCursor();
927
-
928
-			return $size;
929
-		}
930
-		return -1;
931
-	}
932
-
933
-	/**
934
-	 * calculate the size of a folder and set it in the cache
935
-	 *
936
-	 * @param string $path
937
-	 * @param array $entry (optional) meta data of the folder
938
-	 * @return int
939
-	 */
940
-	public function calculateFolderSize($path, $entry = null) {
941
-		$totalSize = 0;
942
-		if (is_null($entry) or !isset($entry['fileid'])) {
943
-			$entry = $this->get($path);
944
-		}
945
-		if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
946
-			$id = $entry['fileid'];
947
-
948
-			$query = $this->getQueryBuilder();
949
-			$query->selectAlias($query->func()->sum('size'), 'f1')
950
-				->selectAlias($query->func()->min('size'), 'f2')
951
-				->from('filecache')
952
-				->whereStorageId()
953
-				->whereParent($id);
954
-
955
-			$result = $query->execute();
956
-			$row = $result->fetch();
957
-			$result->closeCursor();
958
-
959
-			if ($row) {
960
-				[$sum, $min] = array_values($row);
961
-				$sum = 0 + $sum;
962
-				$min = 0 + $min;
963
-				if ($min === -1) {
964
-					$totalSize = $min;
965
-				} else {
966
-					$totalSize = $sum;
967
-				}
968
-				if ($entry['size'] !== $totalSize) {
969
-					$this->update($id, ['size' => $totalSize]);
970
-				}
971
-			}
972
-		}
973
-		return $totalSize;
974
-	}
975
-
976
-	/**
977
-	 * get all file ids on the files on the storage
978
-	 *
979
-	 * @return int[]
980
-	 */
981
-	public function getAll() {
982
-		$query = $this->getQueryBuilder();
983
-		$query->select('fileid')
984
-			->from('filecache')
985
-			->whereStorageId();
986
-
987
-		$result = $query->execute();
988
-		$files = $result->fetchAll(\PDO::FETCH_COLUMN);
989
-		$result->closeCursor();
990
-
991
-		return array_map(function ($id) {
992
-			return (int)$id;
993
-		}, $files);
994
-	}
995
-
996
-	/**
997
-	 * find a folder in the cache which has not been fully scanned
998
-	 *
999
-	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
1000
-	 * use the one with the highest id gives the best result with the background scanner, since that is most
1001
-	 * likely the folder where we stopped scanning previously
1002
-	 *
1003
-	 * @return string|bool the path of the folder or false when no folder matched
1004
-	 */
1005
-	public function getIncomplete() {
1006
-		$query = $this->getQueryBuilder();
1007
-		$query->select('path')
1008
-			->from('filecache')
1009
-			->whereStorageId()
1010
-			->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
1011
-			->orderBy('fileid', 'DESC')
1012
-			->setMaxResults(1);
1013
-
1014
-		$result = $query->execute();
1015
-		$path = $result->fetchOne();
1016
-		$result->closeCursor();
1017
-
1018
-		return $path;
1019
-	}
1020
-
1021
-	/**
1022
-	 * get the path of a file on this storage by it's file id
1023
-	 *
1024
-	 * @param int $id the file id of the file or folder to search
1025
-	 * @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
1026
-	 */
1027
-	public function getPathById($id) {
1028
-		$query = $this->getQueryBuilder();
1029
-		$query->select('path')
1030
-			->from('filecache')
1031
-			->whereStorageId()
1032
-			->whereFileId($id);
1033
-
1034
-		$result = $query->execute();
1035
-		$path = $result->fetchOne();
1036
-		$result->closeCursor();
1037
-
1038
-		if ($path === false) {
1039
-			return null;
1040
-		}
1041
-
1042
-		return (string)$path;
1043
-	}
1044
-
1045
-	/**
1046
-	 * get the storage id of the storage for a file and the internal path of the file
1047
-	 * unlike getPathById this does not limit the search to files on this storage and
1048
-	 * instead does a global search in the cache table
1049
-	 *
1050
-	 * @param int $id
1051
-	 * @return array first element holding the storage id, second the path
1052
-	 * @deprecated use getPathById() instead
1053
-	 */
1054
-	public static function getById($id) {
1055
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
1056
-		$query->select('path', 'storage')
1057
-			->from('filecache')
1058
-			->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
1059
-
1060
-		$result = $query->execute();
1061
-		$row = $result->fetch();
1062
-		$result->closeCursor();
1063
-
1064
-		if ($row) {
1065
-			$numericId = $row['storage'];
1066
-			$path = $row['path'];
1067
-		} else {
1068
-			return null;
1069
-		}
1070
-
1071
-		if ($id = Storage::getStorageId($numericId)) {
1072
-			return [$id, $path];
1073
-		} else {
1074
-			return null;
1075
-		}
1076
-	}
1077
-
1078
-	/**
1079
-	 * normalize the given path
1080
-	 *
1081
-	 * @param string $path
1082
-	 * @return string
1083
-	 */
1084
-	public function normalize($path) {
1085
-		return trim(\OC_Util::normalizeUnicode($path), '/');
1086
-	}
1087
-
1088
-	/**
1089
-	 * Copy a file or folder in the cache
1090
-	 *
1091
-	 * @param ICache $sourceCache
1092
-	 * @param ICacheEntry $sourceEntry
1093
-	 * @param string $targetPath
1094
-	 * @return int fileid of copied entry
1095
-	 */
1096
-	public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1097
-		if ($sourceEntry->getId() < 0) {
1098
-			throw new \RuntimeException("Invalid source cache entry on copyFromCache");
1099
-		}
1100
-		$data = $this->cacheEntryToArray($sourceEntry);
1101
-		$fileId = $this->put($targetPath, $data);
1102
-		if ($fileId <= 0) {
1103
-			throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " ");
1104
-		}
1105
-		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1106
-			$folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1107
-			foreach ($folderContent as $subEntry) {
1108
-				$subTargetPath = $targetPath . '/' . $subEntry->getName();
1109
-				$this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1110
-			}
1111
-		}
1112
-		return $fileId;
1113
-	}
1114
-
1115
-	private function cacheEntryToArray(ICacheEntry $entry): array {
1116
-		return [
1117
-			'size' => $entry->getSize(),
1118
-			'mtime' => $entry->getMTime(),
1119
-			'storage_mtime' => $entry->getStorageMTime(),
1120
-			'mimetype' => $entry->getMimeType(),
1121
-			'mimepart' => $entry->getMimePart(),
1122
-			'etag' => $entry->getEtag(),
1123
-			'permissions' => $entry->getPermissions(),
1124
-			'encrypted' => $entry->isEncrypted(),
1125
-			'creation_time' => $entry->getCreationTime(),
1126
-			'upload_time' => $entry->getUploadTime(),
1127
-			'metadata_etag' => $entry->getMetadataEtag(),
1128
-		];
1129
-	}
70
+    use MoveFromCacheTrait {
71
+        MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
72
+    }
73
+
74
+    /**
75
+     * @var array partial data for the cache
76
+     */
77
+    protected $partial = [];
78
+
79
+    /**
80
+     * @var string
81
+     */
82
+    protected $storageId;
83
+
84
+    private $storage;
85
+
86
+    /**
87
+     * @var Storage $storageCache
88
+     */
89
+    protected $storageCache;
90
+
91
+    /** @var IMimeTypeLoader */
92
+    protected $mimetypeLoader;
93
+
94
+    /**
95
+     * @var IDBConnection
96
+     */
97
+    protected $connection;
98
+
99
+    /**
100
+     * @var IEventDispatcher
101
+     */
102
+    protected $eventDispatcher;
103
+
104
+    /** @var QuerySearchHelper */
105
+    protected $querySearchHelper;
106
+
107
+    /**
108
+     * @param IStorage $storage
109
+     */
110
+    public function __construct(IStorage $storage) {
111
+        $this->storageId = $storage->getId();
112
+        $this->storage = $storage;
113
+        if (strlen($this->storageId) > 64) {
114
+            $this->storageId = md5($this->storageId);
115
+        }
116
+
117
+        $this->storageCache = new Storage($storage);
118
+        $this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
119
+        $this->connection = \OC::$server->getDatabaseConnection();
120
+        $this->eventDispatcher = \OC::$server->get(IEventDispatcher::class);
121
+        $this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
122
+    }
123
+
124
+    protected function getQueryBuilder() {
125
+        return new CacheQueryBuilder(
126
+            $this->connection,
127
+            \OC::$server->getSystemConfig(),
128
+            \OC::$server->getLogger(),
129
+            $this
130
+        );
131
+    }
132
+
133
+    /**
134
+     * Get the numeric storage id for this cache's storage
135
+     *
136
+     * @return int
137
+     */
138
+    public function getNumericStorageId() {
139
+        return $this->storageCache->getNumericId();
140
+    }
141
+
142
+    /**
143
+     * get the stored metadata of a file or folder
144
+     *
145
+     * @param string | int $file either the path of a file or folder or the file id for a file or folder
146
+     * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
147
+     */
148
+    public function get($file) {
149
+        $query = $this->getQueryBuilder();
150
+        $query->selectFileCache();
151
+
152
+        if (is_string($file) or $file == '') {
153
+            // normalize file
154
+            $file = $this->normalize($file);
155
+
156
+            $query->whereStorageId()
157
+                ->wherePath($file);
158
+        } else { //file id
159
+            $query->whereFileId($file);
160
+        }
161
+
162
+        $result = $query->execute();
163
+        $data = $result->fetch();
164
+        $result->closeCursor();
165
+
166
+        //merge partial data
167
+        if (!$data and is_string($file) and isset($this->partial[$file])) {
168
+            return $this->partial[$file];
169
+        } elseif (!$data) {
170
+            return $data;
171
+        } else {
172
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
173
+        }
174
+    }
175
+
176
+    /**
177
+     * Create a CacheEntry from database row
178
+     *
179
+     * @param array $data
180
+     * @param IMimeTypeLoader $mimetypeLoader
181
+     * @return CacheEntry
182
+     */
183
+    public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
184
+        //fix types
185
+        $data['fileid'] = (int)$data['fileid'];
186
+        $data['parent'] = (int)$data['parent'];
187
+        $data['size'] = 0 + $data['size'];
188
+        $data['mtime'] = (int)$data['mtime'];
189
+        $data['storage_mtime'] = (int)$data['storage_mtime'];
190
+        $data['encryptedVersion'] = (int)$data['encrypted'];
191
+        $data['encrypted'] = (bool)$data['encrypted'];
192
+        $data['storage_id'] = $data['storage'];
193
+        $data['storage'] = (int)$data['storage'];
194
+        $data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
195
+        $data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
196
+        if ($data['storage_mtime'] == 0) {
197
+            $data['storage_mtime'] = $data['mtime'];
198
+        }
199
+        $data['permissions'] = (int)$data['permissions'];
200
+        if (isset($data['creation_time'])) {
201
+            $data['creation_time'] = (int)$data['creation_time'];
202
+        }
203
+        if (isset($data['upload_time'])) {
204
+            $data['upload_time'] = (int)$data['upload_time'];
205
+        }
206
+        return new CacheEntry($data);
207
+    }
208
+
209
+    /**
210
+     * get the metadata of all files stored in $folder
211
+     *
212
+     * @param string $folder
213
+     * @return ICacheEntry[]
214
+     */
215
+    public function getFolderContents($folder) {
216
+        $fileId = $this->getId($folder);
217
+        return $this->getFolderContentsById($fileId);
218
+    }
219
+
220
+    /**
221
+     * get the metadata of all files stored in $folder
222
+     *
223
+     * @param int $fileId the file id of the folder
224
+     * @return ICacheEntry[]
225
+     */
226
+    public function getFolderContentsById($fileId) {
227
+        if ($fileId > -1) {
228
+            $query = $this->getQueryBuilder();
229
+            $query->selectFileCache()
230
+                ->whereParent($fileId)
231
+                ->orderBy('name', 'ASC');
232
+
233
+            $result = $query->execute();
234
+            $files = $result->fetchAll();
235
+            $result->closeCursor();
236
+
237
+            return array_map(function (array $data) {
238
+                return self::cacheEntryFromData($data, $this->mimetypeLoader);
239
+            }, $files);
240
+        }
241
+        return [];
242
+    }
243
+
244
+    /**
245
+     * insert or update meta data for a file or folder
246
+     *
247
+     * @param string $file
248
+     * @param array $data
249
+     *
250
+     * @return int file id
251
+     * @throws \RuntimeException
252
+     */
253
+    public function put($file, array $data) {
254
+        if (($id = $this->getId($file)) > -1) {
255
+            $this->update($id, $data);
256
+            return $id;
257
+        } else {
258
+            return $this->insert($file, $data);
259
+        }
260
+    }
261
+
262
+    /**
263
+     * insert meta data for a new file or folder
264
+     *
265
+     * @param string $file
266
+     * @param array $data
267
+     *
268
+     * @return int file id
269
+     * @throws \RuntimeException
270
+     */
271
+    public function insert($file, array $data) {
272
+        // normalize file
273
+        $file = $this->normalize($file);
274
+
275
+        if (isset($this->partial[$file])) { //add any saved partial data
276
+            $data = array_merge($this->partial[$file], $data);
277
+            unset($this->partial[$file]);
278
+        }
279
+
280
+        $requiredFields = ['size', 'mtime', 'mimetype'];
281
+        foreach ($requiredFields as $field) {
282
+            if (!isset($data[$field])) { //data not complete save as partial and return
283
+                $this->partial[$file] = $data;
284
+                return -1;
285
+            }
286
+        }
287
+
288
+        $data['path'] = $file;
289
+        if (!isset($data['parent'])) {
290
+            $data['parent'] = $this->getParentId($file);
291
+        }
292
+        $data['name'] = basename($file);
293
+
294
+        [$values, $extensionValues] = $this->normalizeData($data);
295
+        $storageId = $this->getNumericStorageId();
296
+        $values['storage'] = $storageId;
297
+
298
+        try {
299
+            $builder = $this->connection->getQueryBuilder();
300
+            $builder->insert('filecache');
301
+
302
+            foreach ($values as $column => $value) {
303
+                $builder->setValue($column, $builder->createNamedParameter($value));
304
+            }
305
+
306
+            if ($builder->execute()) {
307
+                $fileId = $builder->getLastInsertId();
308
+
309
+                if (count($extensionValues)) {
310
+                    $query = $this->getQueryBuilder();
311
+                    $query->insert('filecache_extended');
312
+
313
+                    $query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
314
+                    foreach ($extensionValues as $column => $value) {
315
+                        $query->setValue($column, $query->createNamedParameter($value));
316
+                    }
317
+                    $query->execute();
318
+                }
319
+
320
+                $event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
321
+                $this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
322
+                $this->eventDispatcher->dispatchTyped($event);
323
+                return $fileId;
324
+            }
325
+        } catch (UniqueConstraintViolationException $e) {
326
+            // entry exists already
327
+            if ($this->connection->inTransaction()) {
328
+                $this->connection->commit();
329
+                $this->connection->beginTransaction();
330
+            }
331
+        }
332
+
333
+        // The file was created in the mean time
334
+        if (($id = $this->getId($file)) > -1) {
335
+            $this->update($id, $data);
336
+            return $id;
337
+        } else {
338
+            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.');
339
+        }
340
+    }
341
+
342
+    /**
343
+     * update the metadata of an existing file or folder in the cache
344
+     *
345
+     * @param int $id the fileid of the existing file or folder
346
+     * @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
347
+     */
348
+    public function update($id, array $data) {
349
+        if (isset($data['path'])) {
350
+            // normalize path
351
+            $data['path'] = $this->normalize($data['path']);
352
+        }
353
+
354
+        if (isset($data['name'])) {
355
+            // normalize path
356
+            $data['name'] = $this->normalize($data['name']);
357
+        }
358
+
359
+        [$values, $extensionValues] = $this->normalizeData($data);
360
+
361
+        if (count($values)) {
362
+            $query = $this->getQueryBuilder();
363
+
364
+            $query->update('filecache')
365
+                ->whereFileId($id)
366
+                ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
367
+                    return $query->expr()->orX(
368
+                        $query->expr()->neq($key, $query->createNamedParameter($value)),
369
+                        $query->expr()->isNull($key)
370
+                    );
371
+                }, array_keys($values), array_values($values))));
372
+
373
+            foreach ($values as $key => $value) {
374
+                $query->set($key, $query->createNamedParameter($value));
375
+            }
376
+
377
+            $query->execute();
378
+        }
379
+
380
+        if (count($extensionValues)) {
381
+            try {
382
+                $query = $this->getQueryBuilder();
383
+                $query->insert('filecache_extended');
384
+
385
+                $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
386
+                foreach ($extensionValues as $column => $value) {
387
+                    $query->setValue($column, $query->createNamedParameter($value));
388
+                }
389
+
390
+                $query->execute();
391
+            } catch (UniqueConstraintViolationException $e) {
392
+                $query = $this->getQueryBuilder();
393
+                $query->update('filecache_extended')
394
+                    ->whereFileId($id)
395
+                    ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
396
+                        return $query->expr()->orX(
397
+                            $query->expr()->neq($key, $query->createNamedParameter($value)),
398
+                            $query->expr()->isNull($key)
399
+                        );
400
+                    }, array_keys($extensionValues), array_values($extensionValues))));
401
+
402
+                foreach ($extensionValues as $key => $value) {
403
+                    $query->set($key, $query->createNamedParameter($value));
404
+                }
405
+
406
+                $query->execute();
407
+            }
408
+        }
409
+
410
+        $path = $this->getPathById($id);
411
+        // path can still be null if the file doesn't exist
412
+        if ($path !== null) {
413
+            $event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
414
+            $this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
415
+            $this->eventDispatcher->dispatchTyped($event);
416
+        }
417
+    }
418
+
419
+    /**
420
+     * extract query parts and params array from data array
421
+     *
422
+     * @param array $data
423
+     * @return array
424
+     */
425
+    protected function normalizeData(array $data): array {
426
+        $fields = [
427
+            'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
428
+            'etag', 'permissions', 'checksum', 'storage'];
429
+        $extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
430
+
431
+        $doNotCopyStorageMTime = false;
432
+        if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
433
+            // this horrific magic tells it to not copy storage_mtime to mtime
434
+            unset($data['mtime']);
435
+            $doNotCopyStorageMTime = true;
436
+        }
437
+
438
+        $params = [];
439
+        $extensionParams = [];
440
+        foreach ($data as $name => $value) {
441
+            if (array_search($name, $fields) !== false) {
442
+                if ($name === 'path') {
443
+                    $params['path_hash'] = md5($value);
444
+                } elseif ($name === 'mimetype') {
445
+                    $params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
446
+                    $value = $this->mimetypeLoader->getId($value);
447
+                } elseif ($name === 'storage_mtime') {
448
+                    if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
449
+                        $params['mtime'] = $value;
450
+                    }
451
+                } elseif ($name === 'encrypted') {
452
+                    if (isset($data['encryptedVersion'])) {
453
+                        $value = $data['encryptedVersion'];
454
+                    } else {
455
+                        // Boolean to integer conversion
456
+                        $value = $value ? 1 : 0;
457
+                    }
458
+                }
459
+                $params[$name] = $value;
460
+            }
461
+            if (array_search($name, $extensionFields) !== false) {
462
+                $extensionParams[$name] = $value;
463
+            }
464
+        }
465
+        return [$params, array_filter($extensionParams)];
466
+    }
467
+
468
+    /**
469
+     * get the file id for a file
470
+     *
471
+     * 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
472
+     *
473
+     * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
474
+     *
475
+     * @param string $file
476
+     * @return int
477
+     */
478
+    public function getId($file) {
479
+        // normalize file
480
+        $file = $this->normalize($file);
481
+
482
+        $query = $this->getQueryBuilder();
483
+        $query->select('fileid')
484
+            ->from('filecache')
485
+            ->whereStorageId()
486
+            ->wherePath($file);
487
+
488
+        $result = $query->execute();
489
+        $id = $result->fetchOne();
490
+        $result->closeCursor();
491
+
492
+        return $id === false ? -1 : (int)$id;
493
+    }
494
+
495
+    /**
496
+     * get the id of the parent folder of a file
497
+     *
498
+     * @param string $file
499
+     * @return int
500
+     */
501
+    public function getParentId($file) {
502
+        if ($file === '') {
503
+            return -1;
504
+        } else {
505
+            $parent = $this->getParentPath($file);
506
+            return (int)$this->getId($parent);
507
+        }
508
+    }
509
+
510
+    private function getParentPath($path) {
511
+        $parent = dirname($path);
512
+        if ($parent === '.') {
513
+            $parent = '';
514
+        }
515
+        return $parent;
516
+    }
517
+
518
+    /**
519
+     * check if a file is available in the cache
520
+     *
521
+     * @param string $file
522
+     * @return bool
523
+     */
524
+    public function inCache($file) {
525
+        return $this->getId($file) != -1;
526
+    }
527
+
528
+    /**
529
+     * remove a file or folder from the cache
530
+     *
531
+     * when removing a folder from the cache all files and folders inside the folder will be removed as well
532
+     *
533
+     * @param string $file
534
+     */
535
+    public function remove($file) {
536
+        $entry = $this->get($file);
537
+
538
+        if ($entry) {
539
+            $query = $this->getQueryBuilder();
540
+            $query->delete('filecache')
541
+                ->whereFileId($entry->getId());
542
+            $query->execute();
543
+
544
+            $query = $this->getQueryBuilder();
545
+            $query->delete('filecache_extended')
546
+                ->whereFileId($entry->getId());
547
+            $query->execute();
548
+
549
+            if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
550
+                $this->removeChildren($entry);
551
+            }
552
+
553
+            $this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
554
+        }
555
+    }
556
+
557
+    /**
558
+     * Get all sub folders of a folder
559
+     *
560
+     * @param ICacheEntry $entry the cache entry of the folder to get the subfolders for
561
+     * @return ICacheEntry[] the cache entries for the subfolders
562
+     */
563
+    private function getSubFolders(ICacheEntry $entry) {
564
+        $children = $this->getFolderContentsById($entry->getId());
565
+        return array_filter($children, function ($child) {
566
+            return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
567
+        });
568
+    }
569
+
570
+    /**
571
+     * Recursively remove all children of a folder
572
+     *
573
+     * @param ICacheEntry $entry the cache entry of the folder to remove the children of
574
+     * @throws \OC\DatabaseException
575
+     */
576
+    private function removeChildren(ICacheEntry $entry) {
577
+        $parentIds = [$entry->getId()];
578
+        $queue = [$entry->getId()];
579
+
580
+        // we walk depth first trough the file tree, removing all filecache_extended attributes while we walk
581
+        // and collecting all folder ids to later use to delete the filecache entries
582
+        while ($entryId = array_pop($queue)) {
583
+            $children = $this->getFolderContentsById($entryId);
584
+            $childIds = array_map(function (ICacheEntry $cacheEntry) {
585
+                return $cacheEntry->getId();
586
+            }, $children);
587
+
588
+            $query = $this->getQueryBuilder();
589
+            $query->delete('filecache_extended')
590
+                ->where($query->expr()->in('fileid', $query->createNamedParameter($childIds, IQueryBuilder::PARAM_INT_ARRAY)));
591
+            $query->execute();
592
+
593
+            /** @var ICacheEntry[] $childFolders */
594
+            $childFolders = array_filter($children, function ($child) {
595
+                return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
596
+            });
597
+            foreach ($childFolders as $folder) {
598
+                $parentIds[] = $folder->getId();
599
+                $queue[] = $folder->getId();
600
+            }
601
+        }
602
+
603
+        $query = $this->getQueryBuilder();
604
+        $query->delete('filecache')
605
+            ->whereParentIn($parentIds);
606
+        $query->execute();
607
+    }
608
+
609
+    /**
610
+     * Move a file or folder in the cache
611
+     *
612
+     * @param string $source
613
+     * @param string $target
614
+     */
615
+    public function move($source, $target) {
616
+        $this->moveFromCache($this, $source, $target);
617
+    }
618
+
619
+    /**
620
+     * Get the storage id and path needed for a move
621
+     *
622
+     * @param string $path
623
+     * @return array [$storageId, $internalPath]
624
+     */
625
+    protected function getMoveInfo($path) {
626
+        return [$this->getNumericStorageId(), $path];
627
+    }
628
+
629
+    /**
630
+     * Move a file or folder in the cache
631
+     *
632
+     * @param ICache $sourceCache
633
+     * @param string $sourcePath
634
+     * @param string $targetPath
635
+     * @throws \OC\DatabaseException
636
+     * @throws \Exception if the given storages have an invalid id
637
+     */
638
+    public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
639
+        if ($sourceCache instanceof Cache) {
640
+            // normalize source and target
641
+            $sourcePath = $this->normalize($sourcePath);
642
+            $targetPath = $this->normalize($targetPath);
643
+
644
+            $sourceData = $sourceCache->get($sourcePath);
645
+            if ($sourceData === false) {
646
+                throw new \Exception('Invalid source storage path: ' . $sourcePath);
647
+            }
648
+
649
+            $sourceId = $sourceData['fileid'];
650
+            $newParentId = $this->getParentId($targetPath);
651
+
652
+            [$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
653
+            [$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
654
+
655
+            if (is_null($sourceStorageId) || $sourceStorageId === false) {
656
+                throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
657
+            }
658
+            if (is_null($targetStorageId) || $targetStorageId === false) {
659
+                throw new \Exception('Invalid target storage id: ' . $targetStorageId);
660
+            }
661
+
662
+            $this->connection->beginTransaction();
663
+            if ($sourceData['mimetype'] === 'httpd/unix-directory') {
664
+                //update all child entries
665
+                $sourceLength = mb_strlen($sourcePath);
666
+                $query = $this->connection->getQueryBuilder();
667
+
668
+                $fun = $query->func();
669
+                $newPathFunction = $fun->concat(
670
+                    $query->createNamedParameter($targetPath),
671
+                    $fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
672
+                );
673
+                $query->update('filecache')
674
+                    ->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
675
+                    ->set('path_hash', $fun->md5($newPathFunction))
676
+                    ->set('path', $newPathFunction)
677
+                    ->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
678
+                    ->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
679
+
680
+                try {
681
+                    $query->execute();
682
+                } catch (\OC\DatabaseException $e) {
683
+                    $this->connection->rollBack();
684
+                    throw $e;
685
+                }
686
+            }
687
+
688
+            $query = $this->getQueryBuilder();
689
+            $query->update('filecache')
690
+                ->set('storage', $query->createNamedParameter($targetStorageId))
691
+                ->set('path', $query->createNamedParameter($targetPath))
692
+                ->set('path_hash', $query->createNamedParameter(md5($targetPath)))
693
+                ->set('name', $query->createNamedParameter(basename($targetPath)))
694
+                ->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
695
+                ->whereFileId($sourceId);
696
+            $query->execute();
697
+
698
+            $this->connection->commit();
699
+
700
+            if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
701
+                $this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
702
+                $event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
703
+                $this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
704
+                $this->eventDispatcher->dispatchTyped($event);
705
+            } else {
706
+                $event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
707
+                $this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
708
+                $this->eventDispatcher->dispatchTyped($event);
709
+            }
710
+        } else {
711
+            $this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
712
+        }
713
+    }
714
+
715
+    /**
716
+     * remove all entries for files that are stored on the storage from the cache
717
+     */
718
+    public function clear() {
719
+        $query = $this->getQueryBuilder();
720
+        $query->delete('filecache')
721
+            ->whereStorageId();
722
+        $query->execute();
723
+
724
+        $query = $this->connection->getQueryBuilder();
725
+        $query->delete('storages')
726
+            ->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
727
+        $query->execute();
728
+    }
729
+
730
+    /**
731
+     * Get the scan status of a file
732
+     *
733
+     * - Cache::NOT_FOUND: File is not in the cache
734
+     * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
735
+     * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
736
+     * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
737
+     *
738
+     * @param string $file
739
+     *
740
+     * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
741
+     */
742
+    public function getStatus($file) {
743
+        // normalize file
744
+        $file = $this->normalize($file);
745
+
746
+        $query = $this->getQueryBuilder();
747
+        $query->select('size')
748
+            ->from('filecache')
749
+            ->whereStorageId()
750
+            ->wherePath($file);
751
+
752
+        $result = $query->execute();
753
+        $size = $result->fetchOne();
754
+        $result->closeCursor();
755
+
756
+        if ($size !== false) {
757
+            if ((int)$size === -1) {
758
+                return self::SHALLOW;
759
+            } else {
760
+                return self::COMPLETE;
761
+            }
762
+        } else {
763
+            if (isset($this->partial[$file])) {
764
+                return self::PARTIAL;
765
+            } else {
766
+                return self::NOT_FOUND;
767
+            }
768
+        }
769
+    }
770
+
771
+    /**
772
+     * search for files matching $pattern
773
+     *
774
+     * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
775
+     * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
776
+     */
777
+    public function search($pattern) {
778
+        // normalize pattern
779
+        $pattern = $this->normalize($pattern);
780
+
781
+        if ($pattern === '%%') {
782
+            return [];
783
+        }
784
+
785
+        $query = $this->getQueryBuilder();
786
+        $query->selectFileCache()
787
+            ->whereStorageId()
788
+            ->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
789
+
790
+        $result = $query->execute();
791
+        $files = $result->fetchAll();
792
+        $result->closeCursor();
793
+
794
+        return array_map(function (array $data) {
795
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
796
+        }, $files);
797
+    }
798
+
799
+    /**
800
+     * @param IResult $result
801
+     * @return CacheEntry[]
802
+     */
803
+    private function searchResultToCacheEntries(IResult $result): array {
804
+        $files = $result->fetchAll();
805
+
806
+        return array_map(function (array $data) {
807
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
808
+        }, $files);
809
+    }
810
+
811
+    /**
812
+     * search for files by mimetype
813
+     *
814
+     * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
815
+     *        where it will search for all mimetypes in the group ('image/*')
816
+     * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
817
+     */
818
+    public function searchByMime($mimetype) {
819
+        $mimeId = $this->mimetypeLoader->getId($mimetype);
820
+
821
+        $query = $this->getQueryBuilder();
822
+        $query->selectFileCache()
823
+            ->whereStorageId();
824
+
825
+        if (strpos($mimetype, '/')) {
826
+            $query->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
827
+        } else {
828
+            $query->andWhere($query->expr()->eq('mimepart', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
829
+        }
830
+
831
+        $result = $query->execute();
832
+        $files = $result->fetchAll();
833
+        $result->closeCursor();
834
+
835
+        return array_map(function (array $data) {
836
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
837
+        }, $files);
838
+    }
839
+
840
+    public function searchQuery(ISearchQuery $searchQuery) {
841
+        $builder = $this->getQueryBuilder();
842
+
843
+        $query = $builder->selectFileCache('file');
844
+
845
+        $query->whereStorageId();
846
+
847
+        if ($this->querySearchHelper->shouldJoinTags($searchQuery->getSearchOperation())) {
848
+            $user = $searchQuery->getUser();
849
+            if ($user === null) {
850
+                throw new \InvalidArgumentException("Searching by tag requires the user to be set in the query");
851
+            }
852
+            $query
853
+                ->innerJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid'))
854
+                ->innerJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX(
855
+                    $builder->expr()->eq('tagmap.type', 'tag.type'),
856
+                    $builder->expr()->eq('tagmap.categoryid', 'tag.id')
857
+                ))
858
+                ->andWhere($builder->expr()->eq('tag.type', $builder->createNamedParameter('files')))
859
+                ->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($user->getUID())));
860
+        }
861
+
862
+        $searchExpr = $this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation());
863
+        if ($searchExpr) {
864
+            $query->andWhere($searchExpr);
865
+        }
866
+
867
+        if ($searchQuery->limitToHome() && ($this instanceof HomeCache)) {
868
+            $query->andWhere($builder->expr()->like('path', $query->expr()->literal('files/%')));
869
+        }
870
+
871
+        $this->querySearchHelper->addSearchOrdersToQuery($query, $searchQuery->getOrder());
872
+
873
+        if ($searchQuery->getLimit()) {
874
+            $query->setMaxResults($searchQuery->getLimit());
875
+        }
876
+        if ($searchQuery->getOffset()) {
877
+            $query->setFirstResult($searchQuery->getOffset());
878
+        }
879
+
880
+        $result = $query->execute();
881
+        $cacheEntries = $this->searchResultToCacheEntries($result);
882
+        $result->closeCursor();
883
+        return $cacheEntries;
884
+    }
885
+
886
+    /**
887
+     * Re-calculate the folder size and the size of all parent folders
888
+     *
889
+     * @param string|boolean $path
890
+     * @param array $data (optional) meta data of the folder
891
+     */
892
+    public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
893
+        $this->calculateFolderSize($path, $data);
894
+        if ($path !== '') {
895
+            $parent = dirname($path);
896
+            if ($parent === '.' or $parent === '/') {
897
+                $parent = '';
898
+            }
899
+            if ($isBackgroundScan) {
900
+                $parentData = $this->get($parent);
901
+                if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
902
+                    $this->correctFolderSize($parent, $parentData, $isBackgroundScan);
903
+                }
904
+            } else {
905
+                $this->correctFolderSize($parent);
906
+            }
907
+        }
908
+    }
909
+
910
+    /**
911
+     * get the incomplete count that shares parent $folder
912
+     *
913
+     * @param int $fileId the file id of the folder
914
+     * @return int
915
+     */
916
+    public function getIncompleteChildrenCount($fileId) {
917
+        if ($fileId > -1) {
918
+            $query = $this->getQueryBuilder();
919
+            $query->select($query->func()->count())
920
+                ->from('filecache')
921
+                ->whereParent($fileId)
922
+                ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
923
+
924
+            $result = $query->execute();
925
+            $size = (int)$result->fetchOne();
926
+            $result->closeCursor();
927
+
928
+            return $size;
929
+        }
930
+        return -1;
931
+    }
932
+
933
+    /**
934
+     * calculate the size of a folder and set it in the cache
935
+     *
936
+     * @param string $path
937
+     * @param array $entry (optional) meta data of the folder
938
+     * @return int
939
+     */
940
+    public function calculateFolderSize($path, $entry = null) {
941
+        $totalSize = 0;
942
+        if (is_null($entry) or !isset($entry['fileid'])) {
943
+            $entry = $this->get($path);
944
+        }
945
+        if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
946
+            $id = $entry['fileid'];
947
+
948
+            $query = $this->getQueryBuilder();
949
+            $query->selectAlias($query->func()->sum('size'), 'f1')
950
+                ->selectAlias($query->func()->min('size'), 'f2')
951
+                ->from('filecache')
952
+                ->whereStorageId()
953
+                ->whereParent($id);
954
+
955
+            $result = $query->execute();
956
+            $row = $result->fetch();
957
+            $result->closeCursor();
958
+
959
+            if ($row) {
960
+                [$sum, $min] = array_values($row);
961
+                $sum = 0 + $sum;
962
+                $min = 0 + $min;
963
+                if ($min === -1) {
964
+                    $totalSize = $min;
965
+                } else {
966
+                    $totalSize = $sum;
967
+                }
968
+                if ($entry['size'] !== $totalSize) {
969
+                    $this->update($id, ['size' => $totalSize]);
970
+                }
971
+            }
972
+        }
973
+        return $totalSize;
974
+    }
975
+
976
+    /**
977
+     * get all file ids on the files on the storage
978
+     *
979
+     * @return int[]
980
+     */
981
+    public function getAll() {
982
+        $query = $this->getQueryBuilder();
983
+        $query->select('fileid')
984
+            ->from('filecache')
985
+            ->whereStorageId();
986
+
987
+        $result = $query->execute();
988
+        $files = $result->fetchAll(\PDO::FETCH_COLUMN);
989
+        $result->closeCursor();
990
+
991
+        return array_map(function ($id) {
992
+            return (int)$id;
993
+        }, $files);
994
+    }
995
+
996
+    /**
997
+     * find a folder in the cache which has not been fully scanned
998
+     *
999
+     * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
1000
+     * use the one with the highest id gives the best result with the background scanner, since that is most
1001
+     * likely the folder where we stopped scanning previously
1002
+     *
1003
+     * @return string|bool the path of the folder or false when no folder matched
1004
+     */
1005
+    public function getIncomplete() {
1006
+        $query = $this->getQueryBuilder();
1007
+        $query->select('path')
1008
+            ->from('filecache')
1009
+            ->whereStorageId()
1010
+            ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
1011
+            ->orderBy('fileid', 'DESC')
1012
+            ->setMaxResults(1);
1013
+
1014
+        $result = $query->execute();
1015
+        $path = $result->fetchOne();
1016
+        $result->closeCursor();
1017
+
1018
+        return $path;
1019
+    }
1020
+
1021
+    /**
1022
+     * get the path of a file on this storage by it's file id
1023
+     *
1024
+     * @param int $id the file id of the file or folder to search
1025
+     * @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
1026
+     */
1027
+    public function getPathById($id) {
1028
+        $query = $this->getQueryBuilder();
1029
+        $query->select('path')
1030
+            ->from('filecache')
1031
+            ->whereStorageId()
1032
+            ->whereFileId($id);
1033
+
1034
+        $result = $query->execute();
1035
+        $path = $result->fetchOne();
1036
+        $result->closeCursor();
1037
+
1038
+        if ($path === false) {
1039
+            return null;
1040
+        }
1041
+
1042
+        return (string)$path;
1043
+    }
1044
+
1045
+    /**
1046
+     * get the storage id of the storage for a file and the internal path of the file
1047
+     * unlike getPathById this does not limit the search to files on this storage and
1048
+     * instead does a global search in the cache table
1049
+     *
1050
+     * @param int $id
1051
+     * @return array first element holding the storage id, second the path
1052
+     * @deprecated use getPathById() instead
1053
+     */
1054
+    public static function getById($id) {
1055
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
1056
+        $query->select('path', 'storage')
1057
+            ->from('filecache')
1058
+            ->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
1059
+
1060
+        $result = $query->execute();
1061
+        $row = $result->fetch();
1062
+        $result->closeCursor();
1063
+
1064
+        if ($row) {
1065
+            $numericId = $row['storage'];
1066
+            $path = $row['path'];
1067
+        } else {
1068
+            return null;
1069
+        }
1070
+
1071
+        if ($id = Storage::getStorageId($numericId)) {
1072
+            return [$id, $path];
1073
+        } else {
1074
+            return null;
1075
+        }
1076
+    }
1077
+
1078
+    /**
1079
+     * normalize the given path
1080
+     *
1081
+     * @param string $path
1082
+     * @return string
1083
+     */
1084
+    public function normalize($path) {
1085
+        return trim(\OC_Util::normalizeUnicode($path), '/');
1086
+    }
1087
+
1088
+    /**
1089
+     * Copy a file or folder in the cache
1090
+     *
1091
+     * @param ICache $sourceCache
1092
+     * @param ICacheEntry $sourceEntry
1093
+     * @param string $targetPath
1094
+     * @return int fileid of copied entry
1095
+     */
1096
+    public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1097
+        if ($sourceEntry->getId() < 0) {
1098
+            throw new \RuntimeException("Invalid source cache entry on copyFromCache");
1099
+        }
1100
+        $data = $this->cacheEntryToArray($sourceEntry);
1101
+        $fileId = $this->put($targetPath, $data);
1102
+        if ($fileId <= 0) {
1103
+            throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " ");
1104
+        }
1105
+        if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1106
+            $folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1107
+            foreach ($folderContent as $subEntry) {
1108
+                $subTargetPath = $targetPath . '/' . $subEntry->getName();
1109
+                $this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1110
+            }
1111
+        }
1112
+        return $fileId;
1113
+    }
1114
+
1115
+    private function cacheEntryToArray(ICacheEntry $entry): array {
1116
+        return [
1117
+            'size' => $entry->getSize(),
1118
+            'mtime' => $entry->getMTime(),
1119
+            'storage_mtime' => $entry->getStorageMTime(),
1120
+            'mimetype' => $entry->getMimeType(),
1121
+            'mimepart' => $entry->getMimePart(),
1122
+            'etag' => $entry->getEtag(),
1123
+            'permissions' => $entry->getPermissions(),
1124
+            'encrypted' => $entry->isEncrypted(),
1125
+            'creation_time' => $entry->getCreationTime(),
1126
+            'upload_time' => $entry->getUploadTime(),
1127
+            'metadata_etag' => $entry->getMetadataEtag(),
1128
+        ];
1129
+    }
1130 1130
 }
Please login to merge, or discard this patch.
Spacing   +32 added lines, -32 removed lines patch added patch discarded remove patch
@@ -182,26 +182,26 @@  discard block
 block discarded – undo
182 182
 	 */
183 183
 	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
184 184
 		//fix types
185
-		$data['fileid'] = (int)$data['fileid'];
186
-		$data['parent'] = (int)$data['parent'];
185
+		$data['fileid'] = (int) $data['fileid'];
186
+		$data['parent'] = (int) $data['parent'];
187 187
 		$data['size'] = 0 + $data['size'];
188
-		$data['mtime'] = (int)$data['mtime'];
189
-		$data['storage_mtime'] = (int)$data['storage_mtime'];
190
-		$data['encryptedVersion'] = (int)$data['encrypted'];
191
-		$data['encrypted'] = (bool)$data['encrypted'];
188
+		$data['mtime'] = (int) $data['mtime'];
189
+		$data['storage_mtime'] = (int) $data['storage_mtime'];
190
+		$data['encryptedVersion'] = (int) $data['encrypted'];
191
+		$data['encrypted'] = (bool) $data['encrypted'];
192 192
 		$data['storage_id'] = $data['storage'];
193
-		$data['storage'] = (int)$data['storage'];
193
+		$data['storage'] = (int) $data['storage'];
194 194
 		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
195 195
 		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
196 196
 		if ($data['storage_mtime'] == 0) {
197 197
 			$data['storage_mtime'] = $data['mtime'];
198 198
 		}
199
-		$data['permissions'] = (int)$data['permissions'];
199
+		$data['permissions'] = (int) $data['permissions'];
200 200
 		if (isset($data['creation_time'])) {
201
-			$data['creation_time'] = (int)$data['creation_time'];
201
+			$data['creation_time'] = (int) $data['creation_time'];
202 202
 		}
203 203
 		if (isset($data['upload_time'])) {
204
-			$data['upload_time'] = (int)$data['upload_time'];
204
+			$data['upload_time'] = (int) $data['upload_time'];
205 205
 		}
206 206
 		return new CacheEntry($data);
207 207
 	}
@@ -234,7 +234,7 @@  discard block
 block discarded – undo
234 234
 			$files = $result->fetchAll();
235 235
 			$result->closeCursor();
236 236
 
237
-			return array_map(function (array $data) {
237
+			return array_map(function(array $data) {
238 238
 				return self::cacheEntryFromData($data, $this->mimetypeLoader);
239 239
 			}, $files);
240 240
 		}
@@ -363,7 +363,7 @@  discard block
 block discarded – undo
363 363
 
364 364
 			$query->update('filecache')
365 365
 				->whereFileId($id)
366
-				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
366
+				->andWhere($query->expr()->orX(...array_map(function($key, $value) use ($query) {
367 367
 					return $query->expr()->orX(
368 368
 						$query->expr()->neq($key, $query->createNamedParameter($value)),
369 369
 						$query->expr()->isNull($key)
@@ -392,7 +392,7 @@  discard block
 block discarded – undo
392 392
 				$query = $this->getQueryBuilder();
393 393
 				$query->update('filecache_extended')
394 394
 					->whereFileId($id)
395
-					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
395
+					->andWhere($query->expr()->orX(...array_map(function($key, $value) use ($query) {
396 396
 						return $query->expr()->orX(
397 397
 							$query->expr()->neq($key, $query->createNamedParameter($value)),
398 398
 							$query->expr()->isNull($key)
@@ -489,7 +489,7 @@  discard block
 block discarded – undo
489 489
 		$id = $result->fetchOne();
490 490
 		$result->closeCursor();
491 491
 
492
-		return $id === false ? -1 : (int)$id;
492
+		return $id === false ? -1 : (int) $id;
493 493
 	}
494 494
 
495 495
 	/**
@@ -503,7 +503,7 @@  discard block
 block discarded – undo
503 503
 			return -1;
504 504
 		} else {
505 505
 			$parent = $this->getParentPath($file);
506
-			return (int)$this->getId($parent);
506
+			return (int) $this->getId($parent);
507 507
 		}
508 508
 	}
509 509
 
@@ -562,7 +562,7 @@  discard block
 block discarded – undo
562 562
 	 */
563 563
 	private function getSubFolders(ICacheEntry $entry) {
564 564
 		$children = $this->getFolderContentsById($entry->getId());
565
-		return array_filter($children, function ($child) {
565
+		return array_filter($children, function($child) {
566 566
 			return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
567 567
 		});
568 568
 	}
@@ -581,7 +581,7 @@  discard block
 block discarded – undo
581 581
 		// and collecting all folder ids to later use to delete the filecache entries
582 582
 		while ($entryId = array_pop($queue)) {
583 583
 			$children = $this->getFolderContentsById($entryId);
584
-			$childIds = array_map(function (ICacheEntry $cacheEntry) {
584
+			$childIds = array_map(function(ICacheEntry $cacheEntry) {
585 585
 				return $cacheEntry->getId();
586 586
 			}, $children);
587 587
 
@@ -591,7 +591,7 @@  discard block
 block discarded – undo
591 591
 			$query->execute();
592 592
 
593 593
 			/** @var ICacheEntry[] $childFolders */
594
-			$childFolders = array_filter($children, function ($child) {
594
+			$childFolders = array_filter($children, function($child) {
595 595
 				return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
596 596
 			});
597 597
 			foreach ($childFolders as $folder) {
@@ -643,7 +643,7 @@  discard block
 block discarded – undo
643 643
 
644 644
 			$sourceData = $sourceCache->get($sourcePath);
645 645
 			if ($sourceData === false) {
646
-				throw new \Exception('Invalid source storage path: ' . $sourcePath);
646
+				throw new \Exception('Invalid source storage path: '.$sourcePath);
647 647
 			}
648 648
 
649 649
 			$sourceId = $sourceData['fileid'];
@@ -653,10 +653,10 @@  discard block
 block discarded – undo
653 653
 			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
654 654
 
655 655
 			if (is_null($sourceStorageId) || $sourceStorageId === false) {
656
-				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
656
+				throw new \Exception('Invalid source storage id: '.$sourceStorageId);
657 657
 			}
658 658
 			if (is_null($targetStorageId) || $targetStorageId === false) {
659
-				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
659
+				throw new \Exception('Invalid target storage id: '.$targetStorageId);
660 660
 			}
661 661
 
662 662
 			$this->connection->beginTransaction();
@@ -675,7 +675,7 @@  discard block
 block discarded – undo
675 675
 					->set('path_hash', $fun->md5($newPathFunction))
676 676
 					->set('path', $newPathFunction)
677 677
 					->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
678
-					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
678
+					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath).'/%')));
679 679
 
680 680
 				try {
681 681
 					$query->execute();
@@ -754,7 +754,7 @@  discard block
 block discarded – undo
754 754
 		$result->closeCursor();
755 755
 
756 756
 		if ($size !== false) {
757
-			if ((int)$size === -1) {
757
+			if ((int) $size === -1) {
758 758
 				return self::SHALLOW;
759 759
 			} else {
760 760
 				return self::COMPLETE;
@@ -791,7 +791,7 @@  discard block
 block discarded – undo
791 791
 		$files = $result->fetchAll();
792 792
 		$result->closeCursor();
793 793
 
794
-		return array_map(function (array $data) {
794
+		return array_map(function(array $data) {
795 795
 			return self::cacheEntryFromData($data, $this->mimetypeLoader);
796 796
 		}, $files);
797 797
 	}
@@ -803,7 +803,7 @@  discard block
 block discarded – undo
803 803
 	private function searchResultToCacheEntries(IResult $result): array {
804 804
 		$files = $result->fetchAll();
805 805
 
806
-		return array_map(function (array $data) {
806
+		return array_map(function(array $data) {
807 807
 			return self::cacheEntryFromData($data, $this->mimetypeLoader);
808 808
 		}, $files);
809 809
 	}
@@ -832,7 +832,7 @@  discard block
 block discarded – undo
832 832
 		$files = $result->fetchAll();
833 833
 		$result->closeCursor();
834 834
 
835
-		return array_map(function (array $data) {
835
+		return array_map(function(array $data) {
836 836
 			return self::cacheEntryFromData($data, $this->mimetypeLoader);
837 837
 		}, $files);
838 838
 	}
@@ -922,7 +922,7 @@  discard block
 block discarded – undo
922 922
 				->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
923 923
 
924 924
 			$result = $query->execute();
925
-			$size = (int)$result->fetchOne();
925
+			$size = (int) $result->fetchOne();
926 926
 			$result->closeCursor();
927 927
 
928 928
 			return $size;
@@ -988,8 +988,8 @@  discard block
 block discarded – undo
988 988
 		$files = $result->fetchAll(\PDO::FETCH_COLUMN);
989 989
 		$result->closeCursor();
990 990
 
991
-		return array_map(function ($id) {
992
-			return (int)$id;
991
+		return array_map(function($id) {
992
+			return (int) $id;
993 993
 		}, $files);
994 994
 	}
995 995
 
@@ -1039,7 +1039,7 @@  discard block
 block discarded – undo
1039 1039
 			return null;
1040 1040
 		}
1041 1041
 
1042
-		return (string)$path;
1042
+		return (string) $path;
1043 1043
 	}
1044 1044
 
1045 1045
 	/**
@@ -1100,12 +1100,12 @@  discard block
 block discarded – undo
1100 1100
 		$data = $this->cacheEntryToArray($sourceEntry);
1101 1101
 		$fileId = $this->put($targetPath, $data);
1102 1102
 		if ($fileId <= 0) {
1103
-			throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " ");
1103
+			throw new \RuntimeException("Failed to copy to ".$targetPath." from cache with source data ".json_encode($data)." ");
1104 1104
 		}
1105 1105
 		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1106 1106
 			$folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1107 1107
 			foreach ($folderContent as $subEntry) {
1108
-				$subTargetPath = $targetPath . '/' . $subEntry->getName();
1108
+				$subTargetPath = $targetPath.'/'.$subEntry->getName();
1109 1109
 				$this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1110 1110
 			}
1111 1111
 		}
Please login to merge, or discard this patch.