Passed
Push — master ( 7a09b7...f1e2fb )
by Robin
14:09 queued 14s
created

Cache::getParentPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

894
					$this->correctFolderSize($parent, /** @scrutinizer ignore-type */ $parentData, $isBackgroundScan);
Loading history...
895
				}
896
			} else {
897
				$this->correctFolderSize($parent);
898
			}
899
		}
900
	}
901
902
	/**
903
	 * get the incomplete count that shares parent $folder
904
	 *
905
	 * @param int $fileId the file id of the folder
906
	 * @return int
907
	 */
908
	public function getIncompleteChildrenCount($fileId) {
909
		if ($fileId > -1) {
910
			$query = $this->getQueryBuilder();
911
			$query->select($query->func()->count())
912
				->from('filecache')
913
				->whereParent($fileId)
914
				->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
915
916
			$result = $query->execute();
917
			$size = (int)$result->fetchOne();
918
			$result->closeCursor();
919
920
			return $size;
921
		}
922
		return -1;
923
	}
924
925
	/**
926
	 * calculate the size of a folder and set it in the cache
927
	 *
928
	 * @param string $path
929
	 * @param array $entry (optional) meta data of the folder
930
	 * @return int
931
	 */
932
	public function calculateFolderSize($path, $entry = null) {
933
		$totalSize = 0;
934
		if (is_null($entry) or !isset($entry['fileid'])) {
935
			$entry = $this->get($path);
936
		}
937
		if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
938
			$id = $entry['fileid'];
939
940
			$query = $this->getQueryBuilder();
941
			$query->selectAlias($query->func()->sum('size'), 'f1')
942
				->selectAlias($query->func()->min('size'), 'f2')
943
				->from('filecache')
944
				->whereStorageId()
945
				->whereParent($id);
946
947
			$result = $query->execute();
948
			$row = $result->fetch();
949
			$result->closeCursor();
950
951
			if ($row) {
952
				[$sum, $min] = array_values($row);
953
				$sum = 0 + $sum;
954
				$min = 0 + $min;
955
				if ($min === -1) {
956
					$totalSize = $min;
957
				} else {
958
					$totalSize = $sum;
959
				}
960
				if ($entry['size'] !== $totalSize) {
961
					$this->update($id, ['size' => $totalSize]);
962
				}
963
			}
964
		}
965
		return $totalSize;
966
	}
967
968
	/**
969
	 * get all file ids on the files on the storage
970
	 *
971
	 * @return int[]
972
	 */
973
	public function getAll() {
974
		$query = $this->getQueryBuilder();
975
		$query->select('fileid')
976
			->from('filecache')
977
			->whereStorageId();
978
979
		$result = $query->execute();
980
		$files = $result->fetchAll(\PDO::FETCH_COLUMN);
981
		$result->closeCursor();
982
983
		return array_map(function ($id) {
984
			return (int)$id;
985
		}, $files);
986
	}
987
988
	/**
989
	 * find a folder in the cache which has not been fully scanned
990
	 *
991
	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
992
	 * use the one with the highest id gives the best result with the background scanner, since that is most
993
	 * likely the folder where we stopped scanning previously
994
	 *
995
	 * @return string|bool the path of the folder or false when no folder matched
996
	 */
997
	public function getIncomplete() {
998
		$query = $this->getQueryBuilder();
999
		$query->select('path')
1000
			->from('filecache')
1001
			->whereStorageId()
1002
			->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
1003
			->orderBy('fileid', 'DESC')
1004
			->setMaxResults(1);
1005
1006
		$result = $query->execute();
1007
		$path = $result->fetchOne();
1008
		$result->closeCursor();
1009
1010
		return $path;
1011
	}
1012
1013
	/**
1014
	 * get the path of a file on this storage by it's file id
1015
	 *
1016
	 * @param int $id the file id of the file or folder to search
1017
	 * @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
1018
	 */
1019
	public function getPathById($id) {
1020
		$query = $this->getQueryBuilder();
1021
		$query->select('path')
1022
			->from('filecache')
1023
			->whereStorageId()
1024
			->whereFileId($id);
1025
1026
		$result = $query->execute();
1027
		$path = $result->fetchOne();
1028
		$result->closeCursor();
1029
1030
		if ($path === false) {
1031
			return null;
1032
		}
1033
1034
		return (string) $path;
1035
	}
1036
1037
	/**
1038
	 * get the storage id of the storage for a file and the internal path of the file
1039
	 * unlike getPathById this does not limit the search to files on this storage and
1040
	 * instead does a global search in the cache table
1041
	 *
1042
	 * @param int $id
1043
	 * @return array first element holding the storage id, second the path
1044
	 * @deprecated use getPathById() instead
1045
	 */
1046
	public static function getById($id) {
1047
		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
1048
		$query->select('path', 'storage')
1049
			->from('filecache')
1050
			->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
1051
1052
		$result = $query->execute();
1053
		$row = $result->fetch();
1054
		$result->closeCursor();
1055
1056
		if ($row) {
1057
			$numericId = $row['storage'];
1058
			$path = $row['path'];
1059
		} else {
1060
			return null;
1061
		}
1062
1063
		if ($id = Storage::getStorageId($numericId)) {
1064
			return [$id, $path];
1065
		} else {
1066
			return null;
1067
		}
1068
	}
1069
1070
	/**
1071
	 * normalize the given path
1072
	 *
1073
	 * @param string $path
1074
	 * @return string
1075
	 */
1076
	public function normalize($path) {
1077
		return trim(\OC_Util::normalizeUnicode($path), '/');
1078
	}
1079
1080
	/**
1081
	 * Copy a file or folder in the cache
1082
	 *
1083
	 * @param ICache $sourceCache
1084
	 * @param ICacheEntry $sourceEntry
1085
	 * @param string $targetPath
1086
	 * @return int fileid of copied entry
1087
	 */
1088
	public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1089
		if ($sourceEntry->getId() < 0) {
1090
			throw new \RuntimeException("Invalid source cache entry on copyFromCache");
1091
		}
1092
		$data = $this->cacheEntryToArray($sourceEntry);
1093
		$fileId = $this->put($targetPath, $data);
1094
		if ($fileId <= 0) {
1095
			throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " ");
1096
		}
1097
		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1098
			$folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1099
			foreach ($folderContent as $subEntry) {
1100
				$subTargetPath = $targetPath . '/' . $subEntry->getName();
1101
				$this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1102
			}
1103
		}
1104
		return $fileId;
1105
	}
1106
1107
	private function cacheEntryToArray(ICacheEntry $entry): array {
1108
		return [
1109
			'size' => $entry->getSize(),
1110
			'mtime' => $entry->getMTime(),
1111
			'storage_mtime' => $entry->getStorageMTime(),
1112
			'mimetype' => $entry->getMimeType(),
1113
			'mimepart' => $entry->getMimePart(),
1114
			'etag' => $entry->getEtag(),
1115
			'permissions' => $entry->getPermissions(),
1116
			'encrypted' => $entry->isEncrypted(),
1117
			'creation_time' => $entry->getCreationTime(),
1118
			'upload_time' => $entry->getUploadTime(),
1119
			'metadata_etag' => $entry->getMetadataEtag(),
1120
		];
1121
	}
1122
}
1123