Passed
Push — master ( 495329...2abeff )
by Daniel
17:26 queued 12s
created

Cache::copyFromCache()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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

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