Passed
Push — master ( 4c60ff...437d93 )
by Julius
15:25 queued 12s
created

Cache::normalizeData()   C

Complexity

Conditions 14
Paths 38

Size

Total Lines 41
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 29
nc 38
nop 1
dl 0
loc 41
rs 6.2666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Andreas Fischer <[email protected]>
6
 * @author Ari Selseng <[email protected]>
7
 * @author Artem Kochnev <[email protected]>
8
 * @author Björn Schießle <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Daniel Kesselberg <[email protected]>
11
 * @author Florin Peter <[email protected]>
12
 * @author Frédéric Fortier <[email protected]>
13
 * @author Jens-Christian Fischer <[email protected]>
14
 * @author Joas Schilling <[email protected]>
15
 * @author John Molakvoæ <[email protected]>
16
 * @author Jörn Friedrich Dreyer <[email protected]>
17
 * @author Lukas Reschke <[email protected]>
18
 * @author Michael Gapczynski <[email protected]>
19
 * @author Morris Jobke <[email protected]>
20
 * @author Robin Appelman <[email protected]>
21
 * @author Robin McCorkell <[email protected]>
22
 * @author Roeland Jago Douma <[email protected]>
23
 * @author Vincent Petry <[email protected]>
24
 *
25
 * @license AGPL-3.0
26
 *
27
 * This code is free software: you can redistribute it and/or modify
28
 * it under the terms of the GNU Affero General Public License, version 3,
29
 * as published by the Free Software Foundation.
30
 *
31
 * This program is distributed in the hope that it will be useful,
32
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
33
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
34
 * GNU Affero General Public License for more details.
35
 *
36
 * You should have received a copy of the GNU Affero General Public License, version 3,
37
 * along with this program. If not, see <http://www.gnu.org/licenses/>
38
 *
39
 */
40
namespace OC\Files\Cache;
41
42
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
43
use OC\Files\Search\SearchComparison;
44
use OC\Files\Search\SearchQuery;
45
use OCP\DB\QueryBuilder\IQueryBuilder;
46
use OCP\EventDispatcher\IEventDispatcher;
47
use OCP\Files\Cache\CacheEntryInsertedEvent;
48
use OCP\Files\Cache\CacheEntryUpdatedEvent;
49
use OCP\Files\Cache\CacheInsertEvent;
50
use OCP\Files\Cache\CacheEntryRemovedEvent;
51
use OCP\Files\Cache\CacheUpdateEvent;
52
use OCP\Files\Cache\ICache;
53
use OCP\Files\Cache\ICacheEntry;
54
use OCP\Files\FileInfo;
55
use OCP\Files\IMimeTypeLoader;
56
use OCP\Files\Search\ISearchComparison;
57
use OCP\Files\Search\ISearchOperator;
58
use OCP\Files\Search\ISearchQuery;
59
use OCP\Files\Storage\IStorage;
60
use OCP\IDBConnection;
61
62
/**
63
 * Metadata cache for a storage
64
 *
65
 * The cache stores the metadata for all files and folders in a storage and is kept up to date trough the following mechanisms:
66
 *
67
 * - Scanner: scans the storage and updates the cache where needed
68
 * - Watcher: checks for changes made to the filesystem outside of the ownCloud instance and rescans files and folder when a change is detected
69
 * - Updater: listens to changes made to the filesystem inside of the ownCloud instance and updates the cache where needed
70
 * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
71
 */
72
class Cache implements ICache {
73
	use MoveFromCacheTrait {
74
		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
75
	}
76
77
	/**
78
	 * @var array partial data for the cache
79
	 */
80
	protected $partial = [];
81
82
	/**
83
	 * @var string
84
	 */
85
	protected $storageId;
86
87
	private $storage;
88
89
	/**
90
	 * @var Storage $storageCache
91
	 */
92
	protected $storageCache;
93
94
	/** @var IMimeTypeLoader */
95
	protected $mimetypeLoader;
96
97
	/**
98
	 * @var IDBConnection
99
	 */
100
	protected $connection;
101
102
	/**
103
	 * @var IEventDispatcher
104
	 */
105
	protected $eventDispatcher;
106
107
	/** @var QuerySearchHelper */
108
	protected $querySearchHelper;
109
110
	/**
111
	 * @param IStorage $storage
112
	 */
113
	public function __construct(IStorage $storage) {
114
		$this->storageId = $storage->getId();
115
		$this->storage = $storage;
116
		if (strlen($this->storageId) > 64) {
117
			$this->storageId = md5($this->storageId);
118
		}
119
120
		$this->storageCache = new Storage($storage);
121
		$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
122
		$this->connection = \OC::$server->getDatabaseConnection();
123
		$this->eventDispatcher = \OC::$server->get(IEventDispatcher::class);
124
		$this->querySearchHelper = \OC::$server->query(QuerySearchHelper::class);
125
	}
126
127
	protected function getQueryBuilder() {
128
		return new CacheQueryBuilder(
129
			$this->connection,
130
			\OC::$server->getSystemConfig(),
131
			\OC::$server->getLogger()
132
		);
133
	}
134
135
	/**
136
	 * Get the numeric storage id for this cache's storage
137
	 *
138
	 * @return int
139
	 */
140
	public function getNumericStorageId() {
141
		return $this->storageCache->getNumericId();
142
	}
143
144
	/**
145
	 * get the stored metadata of a file or folder
146
	 *
147
	 * @param string | int $file either the path of a file or folder or the file id for a file or folder
148
	 * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
149
	 */
150
	public function get($file) {
151
		$query = $this->getQueryBuilder();
152
		$query->selectFileCache();
153
154
		if (is_string($file) or $file == '') {
155
			// normalize file
156
			$file = $this->normalize($file);
157
158
			$query->whereStorageId($this->getNumericStorageId())
159
				->wherePath($file);
160
		} else { //file id
161
			$query->whereFileId($file);
162
		}
163
164
		$result = $query->execute();
165
		$data = $result->fetch();
166
		$result->closeCursor();
167
168
		//merge partial data
169
		if (!$data and is_string($file) and isset($this->partial[$file])) {
170
			return $this->partial[$file];
171
		} elseif (!$data) {
172
			return $data;
173
		} else {
174
			return self::cacheEntryFromData($data, $this->mimetypeLoader);
175
		}
176
	}
177
178
	/**
179
	 * Create a CacheEntry from database row
180
	 *
181
	 * @param array $data
182
	 * @param IMimeTypeLoader $mimetypeLoader
183
	 * @return CacheEntry
184
	 */
185
	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
186
		//fix types
187
		$data['fileid'] = (int)$data['fileid'];
188
		$data['parent'] = (int)$data['parent'];
189
		$data['size'] = 0 + $data['size'];
190
		$data['mtime'] = (int)$data['mtime'];
191
		$data['storage_mtime'] = (int)$data['storage_mtime'];
192
		$data['encryptedVersion'] = (int)$data['encrypted'];
193
		$data['encrypted'] = (bool)$data['encrypted'];
194
		$data['storage_id'] = $data['storage'];
195
		$data['storage'] = (int)$data['storage'];
196
		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
197
		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
198
		if ($data['storage_mtime'] == 0) {
199
			$data['storage_mtime'] = $data['mtime'];
200
		}
201
		$data['permissions'] = (int)$data['permissions'];
202
		if (isset($data['creation_time'])) {
203
			$data['creation_time'] = (int)$data['creation_time'];
204
		}
205
		if (isset($data['upload_time'])) {
206
			$data['upload_time'] = (int)$data['upload_time'];
207
		}
208
		return new CacheEntry($data);
209
	}
210
211
	/**
212
	 * get the metadata of all files stored in $folder
213
	 *
214
	 * @param string $folder
215
	 * @return ICacheEntry[]
216
	 */
217
	public function getFolderContents($folder) {
218
		$fileId = $this->getId($folder);
219
		return $this->getFolderContentsById($fileId);
220
	}
221
222
	/**
223
	 * get the metadata of all files stored in $folder
224
	 *
225
	 * @param int $fileId the file id of the folder
226
	 * @return ICacheEntry[]
227
	 */
228
	public function getFolderContentsById($fileId) {
229
		if ($fileId > -1) {
230
			$query = $this->getQueryBuilder();
231
			$query->selectFileCache()
232
				->whereParent($fileId)
233
				->orderBy('name', 'ASC');
234
235
			$result = $query->execute();
236
			$files = $result->fetchAll();
237
			$result->closeCursor();
238
239
			return array_map(function (array $data) {
240
				return self::cacheEntryFromData($data, $this->mimetypeLoader);
241
			}, $files);
242
		}
243
		return [];
244
	}
245
246
	/**
247
	 * insert or update meta data for a file or folder
248
	 *
249
	 * @param string $file
250
	 * @param array $data
251
	 *
252
	 * @return int file id
253
	 * @throws \RuntimeException
254
	 */
255
	public function put($file, array $data) {
256
		if (($id = $this->getId($file)) > -1) {
257
			$this->update($id, $data);
258
			return $id;
259
		} else {
260
			return $this->insert($file, $data);
261
		}
262
	}
263
264
	/**
265
	 * insert meta data for a new file or folder
266
	 *
267
	 * @param string $file
268
	 * @param array $data
269
	 *
270
	 * @return int file id
271
	 * @throws \RuntimeException
272
	 */
273
	public function insert($file, array $data) {
274
		// normalize file
275
		$file = $this->normalize($file);
276
277
		if (isset($this->partial[$file])) { //add any saved partial data
278
			$data = array_merge($this->partial[$file], $data);
279
			unset($this->partial[$file]);
280
		}
281
282
		$requiredFields = ['size', 'mtime', 'mimetype'];
283
		foreach ($requiredFields as $field) {
284
			if (!isset($data[$field])) { //data not complete save as partial and return
285
				$this->partial[$file] = $data;
286
				return -1;
287
			}
288
		}
289
290
		$data['path'] = $file;
291
		if (!isset($data['parent'])) {
292
			$data['parent'] = $this->getParentId($file);
293
		}
294
		$data['name'] = basename($file);
295
296
		[$values, $extensionValues] = $this->normalizeData($data);
297
		$storageId = $this->getNumericStorageId();
298
		$values['storage'] = $storageId;
299
300
		try {
301
			$builder = $this->connection->getQueryBuilder();
302
			$builder->insert('filecache');
303
304
			foreach ($values as $column => $value) {
305
				$builder->setValue($column, $builder->createNamedParameter($value));
306
			}
307
308
			if ($builder->execute()) {
309
				$fileId = $builder->getLastInsertId();
310
311
				if (count($extensionValues)) {
312
					$query = $this->getQueryBuilder();
313
					$query->insert('filecache_extended');
314
315
					$query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
316
					foreach ($extensionValues as $column => $value) {
317
						$query->setValue($column, $query->createNamedParameter($value));
318
					}
319
					$query->execute();
320
				}
321
322
				$event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
323
				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
324
				$this->eventDispatcher->dispatchTyped($event);
325
				return $fileId;
326
			}
327
		} catch (UniqueConstraintViolationException $e) {
328
			// entry exists already
329
			if ($this->connection->inTransaction()) {
330
				$this->connection->commit();
331
				$this->connection->beginTransaction();
332
			}
333
		}
334
335
		// The file was created in the mean time
336
		if (($id = $this->getId($file)) > -1) {
337
			$this->update($id, $data);
338
			return $id;
339
		} else {
340
			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.');
341
		}
342
	}
343
344
	/**
345
	 * update the metadata of an existing file or folder in the cache
346
	 *
347
	 * @param int $id the fileid of the existing file or folder
348
	 * @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
349
	 */
350
	public function update($id, array $data) {
351
		if (isset($data['path'])) {
352
			// normalize path
353
			$data['path'] = $this->normalize($data['path']);
354
		}
355
356
		if (isset($data['name'])) {
357
			// normalize path
358
			$data['name'] = $this->normalize($data['name']);
359
		}
360
361
		[$values, $extensionValues] = $this->normalizeData($data);
362
363
		if (count($values)) {
364
			$query = $this->getQueryBuilder();
365
366
			$query->update('filecache')
367
				->whereFileId($id)
368
				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
369
					return $query->expr()->orX(
370
						$query->expr()->neq($key, $query->createNamedParameter($value)),
371
						$query->expr()->isNull($key)
372
					);
373
				}, array_keys($values), array_values($values))));
374
375
			foreach ($values as $key => $value) {
376
				$query->set($key, $query->createNamedParameter($value));
377
			}
378
379
			$query->execute();
380
		}
381
382
		if (count($extensionValues)) {
383
			try {
384
				$query = $this->getQueryBuilder();
385
				$query->insert('filecache_extended');
386
387
				$query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
388
				foreach ($extensionValues as $column => $value) {
389
					$query->setValue($column, $query->createNamedParameter($value));
390
				}
391
392
				$query->execute();
393
			} catch (UniqueConstraintViolationException $e) {
394
				$query = $this->getQueryBuilder();
395
				$query->update('filecache_extended')
396
					->whereFileId($id)
397
					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
398
						return $query->expr()->orX(
399
							$query->expr()->neq($key, $query->createNamedParameter($value)),
400
							$query->expr()->isNull($key)
401
						);
402
					}, array_keys($extensionValues), array_values($extensionValues))));
403
404
				foreach ($extensionValues as $key => $value) {
405
					$query->set($key, $query->createNamedParameter($value));
406
				}
407
408
				$query->execute();
409
			}
410
		}
411
412
		$path = $this->getPathById($id);
413
		// path can still be null if the file doesn't exist
414
		if ($path !== null) {
415
			$event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
416
			$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
417
			$this->eventDispatcher->dispatchTyped($event);
418
		}
419
	}
420
421
	/**
422
	 * extract query parts and params array from data array
423
	 *
424
	 * @param array $data
425
	 * @return array
426
	 */
427
	protected function normalizeData(array $data): array {
428
		$fields = [
429
			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
430
			'etag', 'permissions', 'checksum', 'storage'];
431
		$extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
432
433
		$doNotCopyStorageMTime = false;
434
		if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
435
			// this horrific magic tells it to not copy storage_mtime to mtime
436
			unset($data['mtime']);
437
			$doNotCopyStorageMTime = true;
438
		}
439
440
		$params = [];
441
		$extensionParams = [];
442
		foreach ($data as $name => $value) {
443
			if (array_search($name, $fields) !== false) {
444
				if ($name === 'path') {
445
					$params['path_hash'] = md5($value);
446
				} elseif ($name === 'mimetype') {
447
					$params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
448
					$value = $this->mimetypeLoader->getId($value);
449
				} elseif ($name === 'storage_mtime') {
450
					if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
451
						$params['mtime'] = $value;
452
					}
453
				} elseif ($name === 'encrypted') {
454
					if (isset($data['encryptedVersion'])) {
455
						$value = $data['encryptedVersion'];
456
					} else {
457
						// Boolean to integer conversion
458
						$value = $value ? 1 : 0;
459
					}
460
				}
461
				$params[$name] = $value;
462
			}
463
			if (array_search($name, $extensionFields) !== false) {
464
				$extensionParams[$name] = $value;
465
			}
466
		}
467
		return [$params, array_filter($extensionParams)];
468
	}
469
470
	/**
471
	 * get the file id for a file
472
	 *
473
	 * 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
474
	 *
475
	 * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
476
	 *
477
	 * @param string $file
478
	 * @return int
479
	 */
480
	public function getId($file) {
481
		// normalize file
482
		$file = $this->normalize($file);
483
484
		$query = $this->getQueryBuilder();
485
		$query->select('fileid')
486
			->from('filecache')
487
			->whereStorageId($this->getNumericStorageId())
488
			->wherePath($file);
489
490
		$result = $query->execute();
491
		$id = $result->fetchOne();
492
		$result->closeCursor();
493
494
		return $id === false ? -1 : (int)$id;
495
	}
496
497
	/**
498
	 * get the id of the parent folder of a file
499
	 *
500
	 * @param string $file
501
	 * @return int
502
	 */
503
	public function getParentId($file) {
504
		if ($file === '') {
505
			return -1;
506
		} else {
507
			$parent = $this->getParentPath($file);
508
			return (int)$this->getId($parent);
509
		}
510
	}
511
512
	private function getParentPath($path) {
513
		$parent = dirname($path);
514
		if ($parent === '.') {
515
			$parent = '';
516
		}
517
		return $parent;
518
	}
519
520
	/**
521
	 * check if a file is available in the cache
522
	 *
523
	 * @param string $file
524
	 * @return bool
525
	 */
526
	public function inCache($file) {
527
		return $this->getId($file) != -1;
528
	}
529
530
	/**
531
	 * remove a file or folder from the cache
532
	 *
533
	 * when removing a folder from the cache all files and folders inside the folder will be removed as well
534
	 *
535
	 * @param string $file
536
	 */
537
	public function remove($file) {
538
		$entry = $this->get($file);
539
540
		if ($entry) {
541
			$query = $this->getQueryBuilder();
542
			$query->delete('filecache')
543
				->whereFileId($entry->getId());
544
			$query->execute();
545
546
			$query = $this->getQueryBuilder();
547
			$query->delete('filecache_extended')
548
				->whereFileId($entry->getId());
549
			$query->execute();
550
551
			if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
552
				$this->removeChildren($entry);
553
			}
554
555
			$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
556
		}
557
	}
558
559
	/**
560
	 * Get all sub folders of a folder
561
	 *
562
	 * @param ICacheEntry $entry the cache entry of the folder to get the subfolders for
563
	 * @return ICacheEntry[] the cache entries for the subfolders
564
	 */
565
	private function getSubFolders(ICacheEntry $entry) {
0 ignored issues
show
Unused Code introduced by
The method getSubFolders() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
566
		$children = $this->getFolderContentsById($entry->getId());
567
		return array_filter($children, function ($child) {
568
			return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
569
		});
570
	}
571
572
	/**
573
	 * Recursively remove all children of a folder
574
	 *
575
	 * @param ICacheEntry $entry the cache entry of the folder to remove the children of
576
	 * @throws \OC\DatabaseException
577
	 */
578
	private function removeChildren(ICacheEntry $entry) {
579
		$parentIds = [$entry->getId()];
580
		$queue = [$entry->getId()];
581
582
		// we walk depth first trough the file tree, removing all filecache_extended attributes while we walk
583
		// and collecting all folder ids to later use to delete the filecache entries
584
		while ($entryId = array_pop($queue)) {
585
			$children = $this->getFolderContentsById($entryId);
586
			$childIds = array_map(function (ICacheEntry $cacheEntry) {
587
				return $cacheEntry->getId();
588
			}, $children);
589
590
			$query = $this->getQueryBuilder();
591
			$query->delete('filecache_extended')
592
				->where($query->expr()->in('fileid', $query->createNamedParameter($childIds, IQueryBuilder::PARAM_INT_ARRAY)));
593
			$query->execute();
594
595
			/** @var ICacheEntry[] $childFolders */
596
			$childFolders = array_filter($children, function ($child) {
597
				return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
598
			});
599
			foreach ($childFolders as $folder) {
600
				$parentIds[] = $folder->getId();
601
				$queue[] = $folder->getId();
602
			}
603
		}
604
605
		$query = $this->getQueryBuilder();
606
		$query->delete('filecache')
607
			->whereParentIn($parentIds);
608
		$query->execute();
609
	}
610
611
	/**
612
	 * Move a file or folder in the cache
613
	 *
614
	 * @param string $source
615
	 * @param string $target
616
	 */
617
	public function move($source, $target) {
618
		$this->moveFromCache($this, $source, $target);
619
	}
620
621
	/**
622
	 * Get the storage id and path needed for a move
623
	 *
624
	 * @param string $path
625
	 * @return array [$storageId, $internalPath]
626
	 */
627
	protected function getMoveInfo($path) {
628
		return [$this->getNumericStorageId(), $path];
629
	}
630
631
	/**
632
	 * Move a file or folder in the cache
633
	 *
634
	 * @param ICache $sourceCache
635
	 * @param string $sourcePath
636
	 * @param string $targetPath
637
	 * @throws \OC\DatabaseException
638
	 * @throws \Exception if the given storages have an invalid id
639
	 */
640
	public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
641
		if ($sourceCache instanceof Cache) {
642
			// normalize source and target
643
			$sourcePath = $this->normalize($sourcePath);
644
			$targetPath = $this->normalize($targetPath);
645
646
			$sourceData = $sourceCache->get($sourcePath);
647
			if ($sourceData === false) {
648
				throw new \Exception('Invalid source storage path: ' . $sourcePath);
649
			}
650
651
			$sourceId = $sourceData['fileid'];
652
			$newParentId = $this->getParentId($targetPath);
653
654
			[$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
655
			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
656
657
			if (is_null($sourceStorageId) || $sourceStorageId === false) {
0 ignored issues
show
introduced by
The condition $sourceStorageId === false is always false.
Loading history...
658
				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
659
			}
660
			if (is_null($targetStorageId) || $targetStorageId === false) {
0 ignored issues
show
introduced by
The condition $targetStorageId === false is always false.
Loading history...
661
				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
662
			}
663
664
			$this->connection->beginTransaction();
665
			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
666
				//update all child entries
667
				$sourceLength = mb_strlen($sourcePath);
668
				$query = $this->connection->getQueryBuilder();
669
670
				$fun = $query->func();
671
				$newPathFunction = $fun->concat(
672
					$query->createNamedParameter($targetPath),
673
					$fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
674
				);
675
				$query->update('filecache')
676
					->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
677
					->set('path_hash', $fun->md5($newPathFunction))
678
					->set('path', $newPathFunction)
679
					->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
680
					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
681
682
				try {
683
					$query->execute();
684
				} catch (\OC\DatabaseException $e) {
685
					$this->connection->rollBack();
686
					throw $e;
687
				}
688
			}
689
690
			$query = $this->getQueryBuilder();
691
			$query->update('filecache')
692
				->set('storage', $query->createNamedParameter($targetStorageId))
693
				->set('path', $query->createNamedParameter($targetPath))
694
				->set('path_hash', $query->createNamedParameter(md5($targetPath)))
695
				->set('name', $query->createNamedParameter(basename($targetPath)))
696
				->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
697
				->whereFileId($sourceId);
698
			$query->execute();
699
700
			$this->connection->commit();
701
702
			if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
703
				$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
704
				$event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
705
				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
706
				$this->eventDispatcher->dispatchTyped($event);
707
			} else {
708
				$event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
709
				$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
710
				$this->eventDispatcher->dispatchTyped($event);
711
			}
712
		} else {
713
			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
714
		}
715
	}
716
717
	/**
718
	 * remove all entries for files that are stored on the storage from the cache
719
	 */
720
	public function clear() {
721
		$query = $this->getQueryBuilder();
722
		$query->delete('filecache')
723
			->whereStorageId($this->getNumericStorageId());
724
		$query->execute();
725
726
		$query = $this->connection->getQueryBuilder();
727
		$query->delete('storages')
728
			->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
729
		$query->execute();
730
	}
731
732
	/**
733
	 * Get the scan status of a file
734
	 *
735
	 * - Cache::NOT_FOUND: File is not in the cache
736
	 * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
737
	 * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
738
	 * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
739
	 *
740
	 * @param string $file
741
	 *
742
	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
743
	 */
744
	public function getStatus($file) {
745
		// normalize file
746
		$file = $this->normalize($file);
747
748
		$query = $this->getQueryBuilder();
749
		$query->select('size')
750
			->from('filecache')
751
			->whereStorageId($this->getNumericStorageId())
752
			->wherePath($file);
753
754
		$result = $query->execute();
755
		$size = $result->fetchOne();
756
		$result->closeCursor();
757
758
		if ($size !== false) {
759
			if ((int)$size === -1) {
760
				return self::SHALLOW;
761
			} else {
762
				return self::COMPLETE;
763
			}
764
		} else {
765
			if (isset($this->partial[$file])) {
766
				return self::PARTIAL;
767
			} else {
768
				return self::NOT_FOUND;
769
			}
770
		}
771
	}
772
773
	/**
774
	 * search for files matching $pattern
775
	 *
776
	 * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
777
	 * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
778
	 */
779
	public function search($pattern) {
780
		$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', $pattern);
781
		return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
782
	}
783
784
	/**
785
	 * search for files by mimetype
786
	 *
787
	 * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
788
	 *        where it will search for all mimetypes in the group ('image/*')
789
	 * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
790
	 */
791
	public function searchByMime($mimetype) {
792
		if (strpos($mimetype, '/') === false) {
793
			$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
794
		} else {
795
			$operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
796
		}
797
		return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
798
	}
799
800
	public function searchQuery(ISearchQuery $searchQuery) {
801
		return current($this->querySearchHelper->searchInCaches($searchQuery, [$this]));
802
	}
803
804
	/**
805
	 * Re-calculate the folder size and the size of all parent folders
806
	 *
807
	 * @param string|boolean $path
808
	 * @param array $data (optional) meta data of the folder
809
	 */
810
	public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
811
		$this->calculateFolderSize($path, $data);
812
		if ($path !== '') {
813
			$parent = dirname($path);
814
			if ($parent === '.' or $parent === '/') {
815
				$parent = '';
816
			}
817
			if ($isBackgroundScan) {
818
				$parentData = $this->get($parent);
819
				if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
820
					$this->correctFolderSize($parent, $parentData, $isBackgroundScan);
0 ignored issues
show
Bug introduced by
$parentData of type OCP\Files\Cache\ICacheEntry|false is incompatible with the type array expected by parameter $data of OC\Files\Cache\Cache::correctFolderSize(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

820
					$this->correctFolderSize($parent, /** @scrutinizer ignore-type */ $parentData, $isBackgroundScan);
Loading history...
821
				}
822
			} else {
823
				$this->correctFolderSize($parent);
824
			}
825
		}
826
	}
827
828
	/**
829
	 * get the incomplete count that shares parent $folder
830
	 *
831
	 * @param int $fileId the file id of the folder
832
	 * @return int
833
	 */
834
	public function getIncompleteChildrenCount($fileId) {
835
		if ($fileId > -1) {
836
			$query = $this->getQueryBuilder();
837
			$query->select($query->func()->count())
838
				->from('filecache')
839
				->whereParent($fileId)
840
				->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
841
842
			$result = $query->execute();
843
			$size = (int)$result->fetchOne();
844
			$result->closeCursor();
845
846
			return $size;
847
		}
848
		return -1;
849
	}
850
851
	/**
852
	 * calculate the size of a folder and set it in the cache
853
	 *
854
	 * @param string $path
855
	 * @param array $entry (optional) meta data of the folder
856
	 * @return int
857
	 */
858
	public function calculateFolderSize($path, $entry = null) {
859
		$totalSize = 0;
860
		if (is_null($entry) or !isset($entry['fileid'])) {
861
			$entry = $this->get($path);
862
		}
863
		if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
864
			$id = $entry['fileid'];
865
866
			$query = $this->getQueryBuilder();
867
			$query->selectAlias($query->func()->sum('size'), 'f1')
868
				->selectAlias($query->func()->min('size'), 'f2')
869
				->from('filecache')
870
				->whereStorageId($this->getNumericStorageId())
871
				->whereParent($id);
872
873
			$result = $query->execute();
874
			$row = $result->fetch();
875
			$result->closeCursor();
876
877
			if ($row) {
878
				[$sum, $min] = array_values($row);
879
				$sum = 0 + $sum;
880
				$min = 0 + $min;
881
				if ($min === -1) {
882
					$totalSize = $min;
883
				} else {
884
					$totalSize = $sum;
885
				}
886
				if ($entry['size'] !== $totalSize) {
887
					$this->update($id, ['size' => $totalSize]);
888
				}
889
			}
890
		}
891
		return $totalSize;
892
	}
893
894
	/**
895
	 * get all file ids on the files on the storage
896
	 *
897
	 * @return int[]
898
	 */
899
	public function getAll() {
900
		$query = $this->getQueryBuilder();
901
		$query->select('fileid')
902
			->from('filecache')
903
			->whereStorageId($this->getNumericStorageId());
904
905
		$result = $query->execute();
906
		$files = $result->fetchAll(\PDO::FETCH_COLUMN);
907
		$result->closeCursor();
908
909
		return array_map(function ($id) {
910
			return (int)$id;
911
		}, $files);
912
	}
913
914
	/**
915
	 * find a folder in the cache which has not been fully scanned
916
	 *
917
	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
918
	 * use the one with the highest id gives the best result with the background scanner, since that is most
919
	 * likely the folder where we stopped scanning previously
920
	 *
921
	 * @return string|bool the path of the folder or false when no folder matched
922
	 */
923
	public function getIncomplete() {
924
		$query = $this->getQueryBuilder();
925
		$query->select('path')
926
			->from('filecache')
927
			->whereStorageId($this->getNumericStorageId())
928
			->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
929
			->orderBy('fileid', 'DESC')
930
			->setMaxResults(1);
931
932
		$result = $query->execute();
933
		$path = $result->fetchOne();
934
		$result->closeCursor();
935
936
		return $path;
937
	}
938
939
	/**
940
	 * get the path of a file on this storage by it's file id
941
	 *
942
	 * @param int $id the file id of the file or folder to search
943
	 * @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
944
	 */
945
	public function getPathById($id) {
946
		$query = $this->getQueryBuilder();
947
		$query->select('path')
948
			->from('filecache')
949
			->whereStorageId($this->getNumericStorageId())
950
			->whereFileId($id);
951
952
		$result = $query->execute();
953
		$path = $result->fetchOne();
954
		$result->closeCursor();
955
956
		if ($path === false) {
957
			return null;
958
		}
959
960
		return (string)$path;
961
	}
962
963
	/**
964
	 * get the storage id of the storage for a file and the internal path of the file
965
	 * unlike getPathById this does not limit the search to files on this storage and
966
	 * instead does a global search in the cache table
967
	 *
968
	 * @param int $id
969
	 * @return array first element holding the storage id, second the path
970
	 * @deprecated use getPathById() instead
971
	 */
972
	public static function getById($id) {
973
		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
974
		$query->select('path', 'storage')
975
			->from('filecache')
976
			->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
977
978
		$result = $query->execute();
979
		$row = $result->fetch();
980
		$result->closeCursor();
981
982
		if ($row) {
983
			$numericId = $row['storage'];
984
			$path = $row['path'];
985
		} else {
986
			return null;
987
		}
988
989
		if ($id = Storage::getStorageId($numericId)) {
990
			return [$id, $path];
991
		} else {
992
			return null;
993
		}
994
	}
995
996
	/**
997
	 * normalize the given path
998
	 *
999
	 * @param string $path
1000
	 * @return string
1001
	 */
1002
	public function normalize($path) {
1003
		return trim(\OC_Util::normalizeUnicode($path), '/');
1004
	}
1005
1006
	/**
1007
	 * Copy a file or folder in the cache
1008
	 *
1009
	 * @param ICache $sourceCache
1010
	 * @param ICacheEntry $sourceEntry
1011
	 * @param string $targetPath
1012
	 * @return int fileid of copied entry
1013
	 */
1014
	public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1015
		if ($sourceEntry->getId() < 0) {
1016
			throw new \RuntimeException("Invalid source cache entry on copyFromCache");
1017
		}
1018
		$data = $this->cacheEntryToArray($sourceEntry);
1019
		$fileId = $this->put($targetPath, $data);
1020
		if ($fileId <= 0) {
1021
			throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " ");
1022
		}
1023
		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1024
			$folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1025
			foreach ($folderContent as $subEntry) {
1026
				$subTargetPath = $targetPath . '/' . $subEntry->getName();
1027
				$this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1028
			}
1029
		}
1030
		return $fileId;
1031
	}
1032
1033
	private function cacheEntryToArray(ICacheEntry $entry): array {
1034
		return [
1035
			'size' => $entry->getSize(),
1036
			'mtime' => $entry->getMTime(),
1037
			'storage_mtime' => $entry->getStorageMTime(),
1038
			'mimetype' => $entry->getMimeType(),
1039
			'mimepart' => $entry->getMimePart(),
1040
			'etag' => $entry->getEtag(),
1041
			'permissions' => $entry->getPermissions(),
1042
			'encrypted' => $entry->isEncrypted(),
1043
			'creation_time' => $entry->getCreationTime(),
1044
			'upload_time' => $entry->getUploadTime(),
1045
			'metadata_etag' => $entry->getMetadataEtag(),
1046
		];
1047
	}
1048
1049
	public function getQueryFilterForStorage(): ISearchOperator {
1050
		return new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $this->getNumericStorageId());
1051
	}
1052
1053
	public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
1054
		if ($rawEntry->getStorageId() === $this->getNumericStorageId()) {
1055
			return $rawEntry;
1056
		} else {
1057
			return null;
1058
		}
1059
	}
1060
}
1061