Cache::insert()   F
last analyzed

Complexity

Conditions 12
Paths 1354

Size

Total Lines 68
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 43
nc 1354
nop 2
dl 0
loc 68
rs 2.8
c 0
b 0
f 0

How to fix   Long Method    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
41
namespace OC\Files\Cache;
42
43
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
44
use OC\Files\Search\SearchComparison;
45
use OC\Files\Search\SearchQuery;
46
use OC\Files\Storage\Wrapper\Encryption;
47
use OCP\DB\QueryBuilder\IQueryBuilder;
48
use OCP\EventDispatcher\IEventDispatcher;
49
use OCP\Files\Cache\CacheEntryInsertedEvent;
50
use OCP\Files\Cache\CacheEntryUpdatedEvent;
51
use OCP\Files\Cache\CacheInsertEvent;
52
use OCP\Files\Cache\CacheEntryRemovedEvent;
53
use OCP\Files\Cache\CacheUpdateEvent;
54
use OCP\Files\Cache\ICache;
55
use OCP\Files\Cache\ICacheEntry;
56
use OCP\Files\FileInfo;
57
use OCP\Files\IMimeTypeLoader;
58
use OCP\Files\Search\ISearchComparison;
59
use OCP\Files\Search\ISearchOperator;
60
use OCP\Files\Search\ISearchQuery;
61
use OCP\Files\Storage\IStorage;
62
use OCP\IDBConnection;
63
use OCP\Util;
64
use Psr\Log\LoggerInterface;
65
66
/**
67
 * Metadata cache for a storage
68
 *
69
 * The cache stores the metadata for all files and folders in a storage and is kept up to date through the following mechanisms:
70
 *
71
 * - Scanner: scans the storage and updates the cache where needed
72
 * - Watcher: checks for changes made to the filesystem outside of the Nextcloud instance and rescans files and folder when a change is detected
73
 * - Updater: listens to changes made to the filesystem inside of the Nextcloud instance and updates the cache where needed
74
 * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
75
 */
76
class Cache implements ICache {
77
	use MoveFromCacheTrait {
78
		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
79
	}
80
81
	/**
82
	 * @var array partial data for the cache
83
	 */
84
	protected $partial = [];
85
86
	/**
87
	 * @var string
88
	 */
89
	protected $storageId;
90
91
	private $storage;
92
93
	/**
94
	 * @var Storage $storageCache
95
	 */
96
	protected $storageCache;
97
98
	/** @var IMimeTypeLoader */
99
	protected $mimetypeLoader;
100
101
	/**
102
	 * @var IDBConnection
103
	 */
104
	protected $connection;
105
106
	/**
107
	 * @var IEventDispatcher
108
	 */
109
	protected $eventDispatcher;
110
111
	/** @var QuerySearchHelper */
112
	protected $querySearchHelper;
113
114
	/**
115
	 * @param IStorage $storage
116
	 */
117
	public function __construct(IStorage $storage) {
118
		$this->storageId = $storage->getId();
119
		$this->storage = $storage;
120
		if (strlen($this->storageId) > 64) {
121
			$this->storageId = md5($this->storageId);
122
		}
123
124
		$this->storageCache = new Storage($storage);
125
		$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
126
		$this->connection = \OC::$server->getDatabaseConnection();
127
		$this->eventDispatcher = \OC::$server->get(IEventDispatcher::class);
128
		$this->querySearchHelper = \OC::$server->query(QuerySearchHelper::class);
129
	}
130
131
	protected function getQueryBuilder() {
132
		return new CacheQueryBuilder(
133
			$this->connection,
134
			\OC::$server->getSystemConfig(),
135
			\OC::$server->get(LoggerInterface::class)
136
		);
137
	}
138
139
	/**
140
	 * Get the numeric storage id for this cache's storage
141
	 *
142
	 * @return int
143
	 */
144
	public function getNumericStorageId() {
145
		return $this->storageCache->getNumericId();
146
	}
147
148
	/**
149
	 * get the stored metadata of a file or folder
150
	 *
151
	 * @param string | int $file either the path of a file or folder or the file id for a file or folder
152
	 * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
153
	 */
154
	public function get($file) {
155
		$query = $this->getQueryBuilder();
156
		$query->selectFileCache();
157
158
		if (is_string($file) || $file == '') {
159
			// normalize file
160
			$file = $this->normalize($file);
161
162
			$query->whereStorageId($this->getNumericStorageId())
163
				->wherePath($file);
164
		} else { //file id
165
			$query->whereFileId($file);
166
		}
167
168
		$result = $query->execute();
169
		$data = $result->fetch();
170
		$result->closeCursor();
171
172
		//merge partial data
173
		if (!$data && is_string($file) && isset($this->partial[$file])) {
174
			return $this->partial[$file];
175
		} elseif (!$data) {
176
			return $data;
177
		} else {
178
			return self::cacheEntryFromData($data, $this->mimetypeLoader);
179
		}
180
	}
181
182
	/**
183
	 * Create a CacheEntry from database row
184
	 *
185
	 * @param array $data
186
	 * @param IMimeTypeLoader $mimetypeLoader
187
	 * @return CacheEntry
188
	 */
189
	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
190
		//fix types
191
		$data['name'] = (string)$data['name'];
192
		$data['path'] = (string)$data['path'];
193
		$data['fileid'] = (int)$data['fileid'];
194
		$data['parent'] = (int)$data['parent'];
195
		$data['size'] = Util::numericToNumber($data['size']);
196
		$data['unencrypted_size'] = Util::numericToNumber($data['unencrypted_size'] ?? 0);
197
		$data['mtime'] = (int)$data['mtime'];
198
		$data['storage_mtime'] = (int)$data['storage_mtime'];
199
		$data['encryptedVersion'] = (int)$data['encrypted'];
200
		$data['encrypted'] = (bool)$data['encrypted'];
201
		$data['storage_id'] = $data['storage'];
202
		$data['storage'] = (int)$data['storage'];
203
		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
204
		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
205
		if ($data['storage_mtime'] == 0) {
206
			$data['storage_mtime'] = $data['mtime'];
207
		}
208
		$data['permissions'] = (int)$data['permissions'];
209
		if (isset($data['creation_time'])) {
210
			$data['creation_time'] = (int)$data['creation_time'];
211
		}
212
		if (isset($data['upload_time'])) {
213
			$data['upload_time'] = (int)$data['upload_time'];
214
		}
215
		return new CacheEntry($data);
216
	}
217
218
	/**
219
	 * get the metadata of all files stored in $folder
220
	 *
221
	 * @param string $folder
222
	 * @return ICacheEntry[]
223
	 */
224
	public function getFolderContents($folder) {
225
		$fileId = $this->getId($folder);
226
		return $this->getFolderContentsById($fileId);
227
	}
228
229
	/**
230
	 * get the metadata of all files stored in $folder
231
	 *
232
	 * @param int $fileId the file id of the folder
233
	 * @return ICacheEntry[]
234
	 */
235
	public function getFolderContentsById($fileId) {
236
		if ($fileId > -1) {
237
			$query = $this->getQueryBuilder();
238
			$query->selectFileCache()
239
				->whereParent($fileId)
240
				->orderBy('name', 'ASC');
241
242
			$result = $query->execute();
243
			$files = $result->fetchAll();
244
			$result->closeCursor();
245
246
			return array_map(function (array $data) {
247
				return self::cacheEntryFromData($data, $this->mimetypeLoader);
248
			}, $files);
249
		}
250
		return [];
251
	}
252
253
	/**
254
	 * insert or update meta data for a file or folder
255
	 *
256
	 * @param string $file
257
	 * @param array $data
258
	 *
259
	 * @return int file id
260
	 * @throws \RuntimeException
261
	 */
262
	public function put($file, array $data) {
263
		if (($id = $this->getId($file)) > -1) {
264
			$this->update($id, $data);
265
			return $id;
266
		} else {
267
			return $this->insert($file, $data);
268
		}
269
	}
270
271
	/**
272
	 * insert meta data for a new file or folder
273
	 *
274
	 * @param string $file
275
	 * @param array $data
276
	 *
277
	 * @return int file id
278
	 * @throws \RuntimeException
279
	 */
280
	public function insert($file, array $data) {
281
		// normalize file
282
		$file = $this->normalize($file);
283
284
		if (isset($this->partial[$file])) { //add any saved partial data
285
			$data = array_merge($this->partial[$file], $data);
286
			unset($this->partial[$file]);
287
		}
288
289
		$requiredFields = ['size', 'mtime', 'mimetype'];
290
		foreach ($requiredFields as $field) {
291
			if (!isset($data[$field])) { //data not complete save as partial and return
292
				$this->partial[$file] = $data;
293
				return -1;
294
			}
295
		}
296
297
		$data['path'] = $file;
298
		if (!isset($data['parent'])) {
299
			$data['parent'] = $this->getParentId($file);
300
		}
301
		$data['name'] = basename($file);
302
303
		[$values, $extensionValues] = $this->normalizeData($data);
304
		$storageId = $this->getNumericStorageId();
305
		$values['storage'] = $storageId;
306
307
		try {
308
			$builder = $this->connection->getQueryBuilder();
309
			$builder->insert('filecache');
310
311
			foreach ($values as $column => $value) {
312
				$builder->setValue($column, $builder->createNamedParameter($value));
313
			}
314
315
			if ($builder->execute()) {
316
				$fileId = $builder->getLastInsertId();
317
318
				if (count($extensionValues)) {
319
					$query = $this->getQueryBuilder();
320
					$query->insert('filecache_extended');
321
322
					$query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
323
					foreach ($extensionValues as $column => $value) {
324
						$query->setValue($column, $query->createNamedParameter($value));
325
					}
326
					$query->execute();
327
				}
328
329
				$event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
330
				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
331
				$this->eventDispatcher->dispatchTyped($event);
332
				return $fileId;
333
			}
334
		} catch (UniqueConstraintViolationException $e) {
335
			// entry exists already
336
			if ($this->connection->inTransaction()) {
337
				$this->connection->commit();
338
				$this->connection->beginTransaction();
339
			}
340
		}
341
342
		// The file was created in the mean time
343
		if (($id = $this->getId($file)) > -1) {
344
			$this->update($id, $data);
345
			return $id;
346
		} else {
347
			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.');
348
		}
349
	}
350
351
	/**
352
	 * update the metadata of an existing file or folder in the cache
353
	 *
354
	 * @param int $id the fileid of the existing file or folder
355
	 * @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
356
	 */
357
	public function update($id, array $data) {
358
		if (isset($data['path'])) {
359
			// normalize path
360
			$data['path'] = $this->normalize($data['path']);
361
		}
362
363
		if (isset($data['name'])) {
364
			// normalize path
365
			$data['name'] = $this->normalize($data['name']);
366
		}
367
368
		[$values, $extensionValues] = $this->normalizeData($data);
369
370
		if (count($values)) {
371
			$query = $this->getQueryBuilder();
372
373
			$query->update('filecache')
374
				->whereFileId($id)
375
				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
376
					return $query->expr()->orX(
377
						$query->expr()->neq($key, $query->createNamedParameter($value)),
378
						$query->expr()->isNull($key)
379
					);
380
				}, array_keys($values), array_values($values))));
381
382
			foreach ($values as $key => $value) {
383
				$query->set($key, $query->createNamedParameter($value));
384
			}
385
386
			$query->execute();
387
		}
388
389
		if (count($extensionValues)) {
390
			try {
391
				$query = $this->getQueryBuilder();
392
				$query->insert('filecache_extended');
393
394
				$query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
395
				foreach ($extensionValues as $column => $value) {
396
					$query->setValue($column, $query->createNamedParameter($value));
397
				}
398
399
				$query->execute();
400
			} catch (UniqueConstraintViolationException $e) {
401
				$query = $this->getQueryBuilder();
402
				$query->update('filecache_extended')
403
					->whereFileId($id)
404
					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
405
						return $query->expr()->orX(
406
							$query->expr()->neq($key, $query->createNamedParameter($value)),
407
							$query->expr()->isNull($key)
408
						);
409
					}, array_keys($extensionValues), array_values($extensionValues))));
410
411
				foreach ($extensionValues as $key => $value) {
412
					$query->set($key, $query->createNamedParameter($value));
413
				}
414
415
				$query->execute();
416
			}
417
		}
418
419
		$path = $this->getPathById($id);
420
		// path can still be null if the file doesn't exist
421
		if ($path !== null) {
422
			$event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
423
			$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
424
			$this->eventDispatcher->dispatchTyped($event);
425
		}
426
	}
427
428
	/**
429
	 * extract query parts and params array from data array
430
	 *
431
	 * @param array $data
432
	 * @return array
433
	 */
434
	protected function normalizeData(array $data): array {
435
		$fields = [
436
			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
437
			'etag', 'permissions', 'checksum', 'storage', 'unencrypted_size'];
438
		$extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
439
440
		$doNotCopyStorageMTime = false;
441
		if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
442
			// this horrific magic tells it to not copy storage_mtime to mtime
443
			unset($data['mtime']);
444
			$doNotCopyStorageMTime = true;
445
		}
446
447
		$params = [];
448
		$extensionParams = [];
449
		foreach ($data as $name => $value) {
450
			if (array_search($name, $fields) !== false) {
451
				if ($name === 'path') {
452
					$params['path_hash'] = md5($value);
453
				} elseif ($name === 'mimetype') {
454
					$params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
455
					$value = $this->mimetypeLoader->getId($value);
456
				} elseif ($name === 'storage_mtime') {
457
					if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
458
						$params['mtime'] = $value;
459
					}
460
				} elseif ($name === 'encrypted') {
461
					if (isset($data['encryptedVersion'])) {
462
						$value = $data['encryptedVersion'];
463
					} else {
464
						// Boolean to integer conversion
465
						$value = $value ? 1 : 0;
466
					}
467
				}
468
				$params[$name] = $value;
469
			}
470
			if (array_search($name, $extensionFields) !== false) {
471
				$extensionParams[$name] = $value;
472
			}
473
		}
474
		return [$params, array_filter($extensionParams)];
475
	}
476
477
	/**
478
	 * get the file id for a file
479
	 *
480
	 * 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
481
	 *
482
	 * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
483
	 *
484
	 * @param string $file
485
	 * @return int
486
	 */
487
	public function getId($file) {
488
		// normalize file
489
		$file = $this->normalize($file);
490
491
		$query = $this->getQueryBuilder();
492
		$query->select('fileid')
493
			->from('filecache')
494
			->whereStorageId($this->getNumericStorageId())
495
			->wherePath($file);
496
497
		$result = $query->execute();
498
		$id = $result->fetchOne();
499
		$result->closeCursor();
500
501
		return $id === false ? -1 : (int)$id;
502
	}
503
504
	/**
505
	 * get the id of the parent folder of a file
506
	 *
507
	 * @param string $file
508
	 * @return int
509
	 */
510
	public function getParentId($file) {
511
		if ($file === '') {
512
			return -1;
513
		} else {
514
			$parent = $this->getParentPath($file);
515
			return (int)$this->getId($parent);
516
		}
517
	}
518
519
	private function getParentPath($path) {
520
		$parent = dirname($path);
521
		if ($parent === '.') {
522
			$parent = '';
523
		}
524
		return $parent;
525
	}
526
527
	/**
528
	 * check if a file is available in the cache
529
	 *
530
	 * @param string $file
531
	 * @return bool
532
	 */
533
	public function inCache($file) {
534
		return $this->getId($file) != -1;
535
	}
536
537
	/**
538
	 * remove a file or folder from the cache
539
	 *
540
	 * when removing a folder from the cache all files and folders inside the folder will be removed as well
541
	 *
542
	 * @param string $file
543
	 */
544
	public function remove($file) {
545
		$entry = $this->get($file);
546
547
		if ($entry instanceof ICacheEntry) {
548
			$query = $this->getQueryBuilder();
549
			$query->delete('filecache')
550
				->whereFileId($entry->getId());
551
			$query->execute();
552
553
			$query = $this->getQueryBuilder();
554
			$query->delete('filecache_extended')
555
				->whereFileId($entry->getId());
556
			$query->execute();
557
558
			if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
559
				$this->removeChildren($entry);
560
			}
561
562
			$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
563
		}
564
	}
565
566
	/**
567
	 * Remove all children of a folder
568
	 *
569
	 * @param ICacheEntry $entry the cache entry of the folder to remove the children of
570
	 * @throws \OC\DatabaseException
571
	 */
572
	private function removeChildren(ICacheEntry $entry) {
573
		$parentIds = [$entry->getId()];
574
		$queue = [$entry->getId()];
575
		$deletedIds = [];
576
		$deletedPaths = [];
577
578
		// we walk depth first through the file tree, removing all filecache_extended attributes while we walk
579
		// and collecting all folder ids to later use to delete the filecache entries
580
		while ($entryId = array_pop($queue)) {
581
			$children = $this->getFolderContentsById($entryId);
582
			$childIds = array_map(function (ICacheEntry $cacheEntry) {
583
				return $cacheEntry->getId();
584
			}, $children);
585
			$childPaths = array_map(function (ICacheEntry $cacheEntry) {
586
				return $cacheEntry->getPath();
587
			}, $children);
588
589
			$deletedIds = array_merge($deletedIds, $childIds);
590
			$deletedPaths = array_merge($deletedPaths, $childPaths);
591
592
			$query = $this->getQueryBuilder();
593
			$query->delete('filecache_extended')
594
				->where($query->expr()->in('fileid', $query->createParameter('childIds')));
595
596
			foreach (array_chunk($childIds, 1000) as $childIdChunk) {
597
				$query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
598
				$query->execute();
599
			}
600
601
			/** @var ICacheEntry[] $childFolders */
602
			$childFolders = array_filter($children, function ($child) {
603
				return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
604
			});
605
			foreach ($childFolders as $folder) {
606
				$parentIds[] = $folder->getId();
607
				$queue[] = $folder->getId();
608
			}
609
		}
610
611
		$query = $this->getQueryBuilder();
612
		$query->delete('filecache')
613
			->whereParentInParameter('parentIds');
614
615
		foreach (array_chunk($parentIds, 1000) as $parentIdChunk) {
616
			$query->setParameter('parentIds', $parentIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
617
			$query->execute();
618
		}
619
620
		foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) {
621
			$cacheEntryRemovedEvent = new CacheEntryRemovedEvent(
622
				$this->storage,
623
				$filePath,
624
				$fileId,
625
				$this->getNumericStorageId()
626
			);
627
			$this->eventDispatcher->dispatchTyped($cacheEntryRemovedEvent);
628
		}
629
	}
630
631
	/**
632
	 * Move a file or folder in the cache
633
	 *
634
	 * @param string $source
635
	 * @param string $target
636
	 */
637
	public function move($source, $target) {
638
		$this->moveFromCache($this, $source, $target);
639
	}
640
641
	/**
642
	 * Get the storage id and path needed for a move
643
	 *
644
	 * @param string $path
645
	 * @return array [$storageId, $internalPath]
646
	 */
647
	protected function getMoveInfo($path) {
648
		return [$this->getNumericStorageId(), $path];
649
	}
650
651
	protected function hasEncryptionWrapper(): bool {
652
		return $this->storage->instanceOfStorage(Encryption::class);
653
	}
654
655
	/**
656
	 * Move a file or folder in the cache
657
	 *
658
	 * @param ICache $sourceCache
659
	 * @param string $sourcePath
660
	 * @param string $targetPath
661
	 * @throws \OC\DatabaseException
662
	 * @throws \Exception if the given storages have an invalid id
663
	 */
664
	public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
665
		if ($sourceCache instanceof Cache) {
666
			// normalize source and target
667
			$sourcePath = $this->normalize($sourcePath);
668
			$targetPath = $this->normalize($targetPath);
669
670
			$sourceData = $sourceCache->get($sourcePath);
671
			if ($sourceData === false) {
672
				throw new \Exception('Invalid source storage path: ' . $sourcePath);
673
			}
674
675
			$sourceId = $sourceData['fileid'];
676
			$newParentId = $this->getParentId($targetPath);
677
678
			[$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
679
			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
680
681
			if (is_null($sourceStorageId) || $sourceStorageId === false) {
0 ignored issues
show
introduced by
The condition $sourceStorageId === false is always false.
Loading history...
682
				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
683
			}
684
			if (is_null($targetStorageId) || $targetStorageId === false) {
0 ignored issues
show
introduced by
The condition $targetStorageId === false is always false.
Loading history...
685
				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
686
			}
687
688
			$this->connection->beginTransaction();
689
			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
690
				//update all child entries
691
				$sourceLength = mb_strlen($sourcePath);
692
				$query = $this->connection->getQueryBuilder();
693
694
				$fun = $query->func();
695
				$newPathFunction = $fun->concat(
696
					$query->createNamedParameter($targetPath),
697
					$fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
698
				);
699
				$query->update('filecache')
700
					->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
701
					->set('path_hash', $fun->md5($newPathFunction))
702
					->set('path', $newPathFunction)
703
					->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
704
					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
705
706
				// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
707
				if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
708
					$query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
709
				}
710
711
				try {
712
					$query->execute();
713
				} catch (\OC\DatabaseException $e) {
714
					$this->connection->rollBack();
715
					throw $e;
716
				}
717
			}
718
719
			$query = $this->getQueryBuilder();
720
			$query->update('filecache')
721
				->set('storage', $query->createNamedParameter($targetStorageId))
722
				->set('path', $query->createNamedParameter($targetPath))
723
				->set('path_hash', $query->createNamedParameter(md5($targetPath)))
724
				->set('name', $query->createNamedParameter(basename($targetPath)))
725
				->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
726
				->whereFileId($sourceId);
727
728
			// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
729
			if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
730
				$query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
731
			}
732
733
			$query->execute();
734
735
			$this->connection->commit();
736
737
			if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
738
				$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
739
				$event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
740
				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
741
				$this->eventDispatcher->dispatchTyped($event);
742
			} else {
743
				$event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
744
				$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
745
				$this->eventDispatcher->dispatchTyped($event);
746
			}
747
		} else {
748
			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
749
		}
750
	}
751
752
	/**
753
	 * remove all entries for files that are stored on the storage from the cache
754
	 */
755
	public function clear() {
756
		$query = $this->getQueryBuilder();
757
		$query->delete('filecache')
758
			->whereStorageId($this->getNumericStorageId());
759
		$query->execute();
760
761
		$query = $this->connection->getQueryBuilder();
762
		$query->delete('storages')
763
			->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
764
		$query->execute();
765
	}
766
767
	/**
768
	 * Get the scan status of a file
769
	 *
770
	 * - Cache::NOT_FOUND: File is not in the cache
771
	 * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
772
	 * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
773
	 * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
774
	 *
775
	 * @param string $file
776
	 *
777
	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
778
	 */
779
	public function getStatus($file) {
780
		// normalize file
781
		$file = $this->normalize($file);
782
783
		$query = $this->getQueryBuilder();
784
		$query->select('size')
785
			->from('filecache')
786
			->whereStorageId($this->getNumericStorageId())
787
			->wherePath($file);
788
789
		$result = $query->execute();
790
		$size = $result->fetchOne();
791
		$result->closeCursor();
792
793
		if ($size !== false) {
794
			if ((int)$size === -1) {
795
				return self::SHALLOW;
796
			} else {
797
				return self::COMPLETE;
798
			}
799
		} else {
800
			if (isset($this->partial[$file])) {
801
				return self::PARTIAL;
802
			} else {
803
				return self::NOT_FOUND;
804
			}
805
		}
806
	}
807
808
	/**
809
	 * search for files matching $pattern
810
	 *
811
	 * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
812
	 * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
813
	 */
814
	public function search($pattern) {
815
		$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', $pattern);
816
		return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
817
	}
818
819
	/**
820
	 * search for files by mimetype
821
	 *
822
	 * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
823
	 *        where it will search for all mimetypes in the group ('image/*')
824
	 * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
825
	 */
826
	public function searchByMime($mimetype) {
827
		if (!str_contains($mimetype, '/')) {
828
			$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
829
		} else {
830
			$operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
831
		}
832
		return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
833
	}
834
835
	public function searchQuery(ISearchQuery $searchQuery) {
836
		return current($this->querySearchHelper->searchInCaches($searchQuery, [$this]));
837
	}
838
839
	/**
840
	 * Re-calculate the folder size and the size of all parent folders
841
	 *
842
	 * @param string|boolean $path
843
	 * @param array $data (optional) meta data of the folder
844
	 */
845
	public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
846
		$this->calculateFolderSize($path, $data);
847
		if ($path !== '') {
848
			$parent = dirname($path);
849
			if ($parent === '.' || $parent === '/') {
850
				$parent = '';
851
			}
852
			if ($isBackgroundScan) {
853
				$parentData = $this->get($parent);
854
				if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
855
					$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

855
					$this->correctFolderSize($parent, /** @scrutinizer ignore-type */ $parentData, $isBackgroundScan);
Loading history...
856
				}
857
			} else {
858
				$this->correctFolderSize($parent);
859
			}
860
		}
861
	}
862
863
	/**
864
	 * get the incomplete count that shares parent $folder
865
	 *
866
	 * @param int $fileId the file id of the folder
867
	 * @return int
868
	 */
869
	public function getIncompleteChildrenCount($fileId) {
870
		if ($fileId > -1) {
871
			$query = $this->getQueryBuilder();
872
			$query->select($query->func()->count())
873
				->from('filecache')
874
				->whereParent($fileId)
875
				->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
876
877
			$result = $query->execute();
878
			$size = (int)$result->fetchOne();
879
			$result->closeCursor();
880
881
			return $size;
882
		}
883
		return -1;
884
	}
885
886
	/**
887
	 * calculate the size of a folder and set it in the cache
888
	 *
889
	 * @param string $path
890
	 * @param array|null|ICacheEntry $entry (optional) meta data of the folder
891
	 * @return int|float
892
	 */
893
	public function calculateFolderSize($path, $entry = null) {
894
		return $this->calculateFolderSizeInner($path, $entry);
895
	}
896
897
898
	/**
899
	 * inner function because we can't add new params to the public function without breaking any child classes
900
	 *
901
	 * @param string $path
902
	 * @param array|null|ICacheEntry $entry (optional) meta data of the folder
903
	 * @param bool $ignoreUnknown don't mark the folder size as unknown if any of it's children are unknown
904
	 * @return int|float
905
	 */
906
	protected function calculateFolderSizeInner(string $path, $entry = null, bool $ignoreUnknown = false) {
907
		$totalSize = 0;
908
		if (is_null($entry) || !isset($entry['fileid'])) {
909
			$entry = $this->get($path);
910
		}
911
		if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
912
			$id = $entry['fileid'];
913
914
			$query = $this->getQueryBuilder();
915
			$query->select('size', 'unencrypted_size')
916
				->from('filecache')
917
				->whereParent($id);
918
			if ($ignoreUnknown) {
919
				$query->andWhere($query->expr()->gte('size', $query->createNamedParameter(0)));
920
			}
921
922
			$result = $query->execute();
923
			$rows = $result->fetchAll();
924
			$result->closeCursor();
925
926
			if ($rows) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rows of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
927
				$sizes = array_map(function (array $row) {
928
					return Util::numericToNumber($row['size']);
929
				}, $rows);
930
				$unencryptedOnlySizes = array_map(function (array $row) {
931
					return Util::numericToNumber($row['unencrypted_size']);
932
				}, $rows);
933
				$unencryptedSizes = array_map(function (array $row) {
934
					return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
935
				}, $rows);
936
937
				$sum = array_sum($sizes);
938
				$min = min($sizes);
939
940
				$unencryptedSum = array_sum($unencryptedSizes);
941
				$unencryptedMin = min($unencryptedSizes);
942
				$unencryptedMax = max($unencryptedOnlySizes);
943
944
				$sum = 0 + $sum;
945
				$min = 0 + $min;
946
				if ($min === -1) {
947
					$totalSize = $min;
948
				} else {
949
					$totalSize = $sum;
950
				}
951
				if ($unencryptedMin === -1 || $min === -1) {
952
					$unencryptedTotal = $unencryptedMin;
953
				} else {
954
					$unencryptedTotal = $unencryptedSum;
955
				}
956
			} else {
957
				$totalSize = 0;
958
				$unencryptedTotal = 0;
959
				$unencryptedMax = 0;
960
			}
961
962
			// only set unencrypted size for a folder if any child entries have it set, or the folder is empty
963
			$shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || $entry['unencrypted_size'] > 0;
964
			if ($entry['size'] !== $totalSize || ($entry['unencrypted_size'] !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) {
965
				if ($shouldWriteUnEncryptedSize) {
966
					// if all children have an unencrypted size of 0, just set the folder unencrypted size to 0 instead of summing the sizes
967
					if ($unencryptedMax === 0) {
968
						$unencryptedTotal = 0;
969
					}
970
971
					$this->update($id, [
972
						'size' => $totalSize,
973
						'unencrypted_size' => $unencryptedTotal,
974
					]);
975
				} else {
976
					$this->update($id, [
977
						'size' => $totalSize,
978
					]);
979
				}
980
			}
981
		}
982
		return $totalSize;
983
	}
984
985
	/**
986
	 * get all file ids on the files on the storage
987
	 *
988
	 * @return int[]
989
	 */
990
	public function getAll() {
991
		$query = $this->getQueryBuilder();
992
		$query->select('fileid')
993
			->from('filecache')
994
			->whereStorageId($this->getNumericStorageId());
995
996
		$result = $query->execute();
997
		$files = $result->fetchAll(\PDO::FETCH_COLUMN);
998
		$result->closeCursor();
999
1000
		return array_map(function ($id) {
1001
			return (int)$id;
1002
		}, $files);
1003
	}
1004
1005
	/**
1006
	 * find a folder in the cache which has not been fully scanned
1007
	 *
1008
	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
1009
	 * use the one with the highest id gives the best result with the background scanner, since that is most
1010
	 * likely the folder where we stopped scanning previously
1011
	 *
1012
	 * @return string|false the path of the folder or false when no folder matched
1013
	 */
1014
	public function getIncomplete() {
1015
		$query = $this->getQueryBuilder();
1016
		$query->select('path')
1017
			->from('filecache')
1018
			->whereStorageId($this->getNumericStorageId())
1019
			->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
1020
			->orderBy('fileid', 'DESC')
1021
			->setMaxResults(1);
1022
1023
		$result = $query->execute();
1024
		$path = $result->fetchOne();
1025
		$result->closeCursor();
1026
1027
		if ($path === false) {
1028
			return false;
1029
		}
1030
1031
		// Make sure Oracle does not continue with null for empty strings
1032
		return (string)$path;
1033
	}
1034
1035
	/**
1036
	 * get the path of a file on this storage by it's file id
1037
	 *
1038
	 * @param int $id the file id of the file or folder to search
1039
	 * @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
1040
	 */
1041
	public function getPathById($id) {
1042
		$query = $this->getQueryBuilder();
1043
		$query->select('path')
1044
			->from('filecache')
1045
			->whereStorageId($this->getNumericStorageId())
1046
			->whereFileId($id);
1047
1048
		$result = $query->execute();
1049
		$path = $result->fetchOne();
1050
		$result->closeCursor();
1051
1052
		if ($path === false) {
1053
			return null;
1054
		}
1055
1056
		return (string)$path;
1057
	}
1058
1059
	/**
1060
	 * get the storage id of the storage for a file and the internal path of the file
1061
	 * unlike getPathById this does not limit the search to files on this storage and
1062
	 * instead does a global search in the cache table
1063
	 *
1064
	 * @param int $id
1065
	 * @return array first element holding the storage id, second the path
1066
	 * @deprecated use getPathById() instead
1067
	 */
1068
	public static function getById($id) {
1069
		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
1070
		$query->select('path', 'storage')
1071
			->from('filecache')
1072
			->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
1073
1074
		$result = $query->execute();
1075
		$row = $result->fetch();
1076
		$result->closeCursor();
1077
1078
		if ($row) {
1079
			$numericId = $row['storage'];
1080
			$path = $row['path'];
1081
		} else {
1082
			return null;
1083
		}
1084
1085
		if ($id = Storage::getStorageId($numericId)) {
1086
			return [$id, $path];
1087
		} else {
1088
			return null;
1089
		}
1090
	}
1091
1092
	/**
1093
	 * normalize the given path
1094
	 *
1095
	 * @param string $path
1096
	 * @return string
1097
	 */
1098
	public function normalize($path) {
1099
		return trim(\OC_Util::normalizeUnicode($path), '/');
1100
	}
1101
1102
	/**
1103
	 * Copy a file or folder in the cache
1104
	 *
1105
	 * @param ICache $sourceCache
1106
	 * @param ICacheEntry $sourceEntry
1107
	 * @param string $targetPath
1108
	 * @return int fileId of copied entry
1109
	 */
1110
	public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1111
		if ($sourceEntry->getId() < 0) {
1112
			throw new \RuntimeException("Invalid source cache entry on copyFromCache");
1113
		}
1114
		$data = $this->cacheEntryToArray($sourceEntry);
1115
1116
		// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1117
		if ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
1118
			$data['encrypted'] = 0;
1119
		}
1120
1121
		$fileId = $this->put($targetPath, $data);
1122
		if ($fileId <= 0) {
1123
			throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " ");
1124
		}
1125
		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1126
			$folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1127
			foreach ($folderContent as $subEntry) {
1128
				$subTargetPath = $targetPath . '/' . $subEntry->getName();
1129
				$this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1130
			}
1131
		}
1132
		return $fileId;
1133
	}
1134
1135
	private function cacheEntryToArray(ICacheEntry $entry): array {
1136
		return [
1137
			'size' => $entry->getSize(),
1138
			'mtime' => $entry->getMTime(),
1139
			'storage_mtime' => $entry->getStorageMTime(),
1140
			'mimetype' => $entry->getMimeType(),
1141
			'mimepart' => $entry->getMimePart(),
1142
			'etag' => $entry->getEtag(),
1143
			'permissions' => $entry->getPermissions(),
1144
			'encrypted' => $entry->isEncrypted(),
1145
			'creation_time' => $entry->getCreationTime(),
1146
			'upload_time' => $entry->getUploadTime(),
1147
			'metadata_etag' => $entry->getMetadataEtag(),
1148
		];
1149
	}
1150
1151
	public function getQueryFilterForStorage(): ISearchOperator {
1152
		return new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $this->getNumericStorageId());
1153
	}
1154
1155
	public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
1156
		if ($rawEntry->getStorageId() === $this->getNumericStorageId()) {
1157
			return $rawEntry;
1158
		} else {
1159
			return null;
1160
		}
1161
	}
1162
}
1163