Passed
Push — master ( ccc0a5...5195be )
by Roeland
10:43 queued 11s
created

Cache::getNumericStorageId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 2
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 Florin Peter <[email protected]>
10
 * @author Frédéric Fortier <[email protected]>
11
 * @author Jens-Christian Fischer <[email protected]>
12
 * @author Joas Schilling <[email protected]>
13
 * @author Jörn Friedrich Dreyer <[email protected]>
14
 * @author Lukas Reschke <[email protected]>
15
 * @author Michael Gapczynski <[email protected]>
16
 * @author Morris Jobke <[email protected]>
17
 * @author Robin Appelman <[email protected]>
18
 * @author Robin McCorkell <[email protected]>
19
 * @author Roeland Jago Douma <[email protected]>
20
 * @author Thomas Müller <[email protected]>
21
 * @author Vincent Petry <[email protected]>
22
 *
23
 * @license AGPL-3.0
24
 *
25
 * This code is free software: you can redistribute it and/or modify
26
 * it under the terms of the GNU Affero General Public License, version 3,
27
 * as published by the Free Software Foundation.
28
 *
29
 * This program is distributed in the hope that it will be useful,
30
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32
 * GNU Affero General Public License for more details.
33
 *
34
 * You should have received a copy of the GNU Affero General Public License, version 3,
35
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
36
 *
37
 */
38
39
namespace OC\Files\Cache;
40
41
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
42
use OCP\DB\QueryBuilder\IQueryBuilder;
43
use Doctrine\DBAL\Driver\Statement;
44
use OCP\Files\Cache\CacheInsertEvent;
45
use OCP\Files\Cache\CacheUpdateEvent;
46
use OCP\Files\Cache\ICache;
47
use OCP\Files\Cache\ICacheEntry;
48
use OCP\Files\FileInfo;
49
use \OCP\Files\IMimeTypeLoader;
50
use OCP\Files\Search\ISearchQuery;
51
use OCP\Files\Storage\IStorage;
52
use OCP\IDBConnection;
53
54
/**
55
 * Metadata cache for a storage
56
 *
57
 * The cache stores the metadata for all files and folders in a storage and is kept up to date trough the following mechanisms:
58
 *
59
 * - Scanner: scans the storage and updates the cache where needed
60
 * - Watcher: checks for changes made to the filesystem outside of the ownCloud instance and rescans files and folder when a change is detected
61
 * - Updater: listens to changes made to the filesystem inside of the ownCloud instance and updates the cache where needed
62
 * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
63
 */
64
class Cache implements ICache {
65
	use MoveFromCacheTrait {
66
		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
67
	}
68
69
	/**
70
	 * @var array partial data for the cache
71
	 */
72
	protected $partial = [];
73
74
	/**
75
	 * @var string
76
	 */
77
	protected $storageId;
78
79
	private $storage;
80
81
	/**
82
	 * @var Storage $storageCache
83
	 */
84
	protected $storageCache;
85
86
	/** @var IMimeTypeLoader */
87
	protected $mimetypeLoader;
88
89
	/**
90
	 * @var IDBConnection
91
	 */
92
	protected $connection;
93
94
	protected $eventDispatcher;
95
96
	/** @var QuerySearchHelper */
97
	protected $querySearchHelper;
98
99
	/**
100
	 * @param IStorage $storage
101
	 */
102
	public function __construct(IStorage $storage) {
103
		$this->storageId = $storage->getId();
104
		$this->storage = $storage;
105
		if (strlen($this->storageId) > 64) {
106
			$this->storageId = md5($this->storageId);
107
		}
108
109
		$this->storageCache = new Storage($storage);
110
		$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
111
		$this->connection = \OC::$server->getDatabaseConnection();
112
		$this->eventDispatcher = \OC::$server->getEventDispatcher();
113
		$this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
114
	}
115
116
	private function getQueryBuilder() {
117
		return new CacheQueryBuilder(
118
			$this->connection,
119
			\OC::$server->getSystemConfig(),
120
			\OC::$server->getLogger(),
121
			$this
122
		);
123
	}
124
125
	/**
126
	 * Get the numeric storage id for this cache's storage
127
	 *
128
	 * @return int
129
	 */
130
	public function getNumericStorageId() {
131
		return $this->storageCache->getNumericId();
132
	}
133
134
	/**
135
	 * get the stored metadata of a file or folder
136
	 *
137
	 * @param string | int $file either the path of a file or folder or the file id for a file or folder
138
	 * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
139
	 */
140
	public function get($file) {
141
		$query = $this->getQueryBuilder();
142
		$query->selectFileCache();
143
144
		if (is_string($file) or $file == '') {
145
			// normalize file
146
			$file = $this->normalize($file);
147
148
			$query->whereStorageId()
149
				->wherePath($file);
150
		} else { //file id
151
			$query->whereFileId($file);
152
		}
153
154
		$data = $query->execute()->fetch();
155
156
		//merge partial data
157
		if (!$data and is_string($file) and isset($this->partial[$file])) {
158
			return $this->partial[$file];
159
		} else if (!$data) {
160
			return $data;
161
		} else {
162
			return self::cacheEntryFromData($data, $this->mimetypeLoader);
163
		}
164
	}
165
166
	/**
167
	 * Create a CacheEntry from database row
168
	 *
169
	 * @param array $data
170
	 * @param IMimeTypeLoader $mimetypeLoader
171
	 * @return CacheEntry
172
	 */
173
	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
174
		//fix types
175
		$data['fileid'] = (int)$data['fileid'];
176
		$data['parent'] = (int)$data['parent'];
177
		$data['size'] = 0 + $data['size'];
178
		$data['mtime'] = (int)$data['mtime'];
179
		$data['storage_mtime'] = (int)$data['storage_mtime'];
180
		$data['encryptedVersion'] = (int)$data['encrypted'];
181
		$data['encrypted'] = (bool)$data['encrypted'];
182
		$data['storage_id'] = $data['storage'];
183
		$data['storage'] = (int)$data['storage'];
184
		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
185
		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
186
		if ($data['storage_mtime'] == 0) {
187
			$data['storage_mtime'] = $data['mtime'];
188
		}
189
		$data['permissions'] = (int)$data['permissions'];
190
		if (isset($data['creation_time'])) {
191
			$data['creation_time'] = (int) $data['creation_time'];
192
		}
193
		if (isset($data['upload_time'])) {
194
			$data['upload_time'] = (int) $data['upload_time'];
195
		}
196
		return new CacheEntry($data);
197
	}
198
199
	/**
200
	 * get the metadata of all files stored in $folder
201
	 *
202
	 * @param string $folder
203
	 * @return ICacheEntry[]
204
	 */
205
	public function getFolderContents($folder) {
206
		$fileId = $this->getId($folder);
207
		return $this->getFolderContentsById($fileId);
208
	}
209
210
	/**
211
	 * get the metadata of all files stored in $folder
212
	 *
213
	 * @param int $fileId the file id of the folder
214
	 * @return ICacheEntry[]
215
	 */
216
	public function getFolderContentsById($fileId) {
217
		if ($fileId > -1) {
218
			$query = $this->getQueryBuilder();
219
			$query->selectFileCache()
220
				->whereParent($fileId)
221
				->orderBy('name', 'ASC');
222
223
			$files = $query->execute()->fetchAll();
224
			return array_map(function (array $data) {
225
				return self::cacheEntryFromData($data, $this->mimetypeLoader);
226
			}, $files);
227
		}
228
		return [];
229
	}
230
231
	/**
232
	 * insert or update meta data for a file or folder
233
	 *
234
	 * @param string $file
235
	 * @param array $data
236
	 *
237
	 * @return int file id
238
	 * @throws \RuntimeException
239
	 */
240
	public function put($file, array $data) {
241
		if (($id = $this->getId($file)) > -1) {
242
			$this->update($id, $data);
243
			return $id;
244
		} else {
245
			return $this->insert($file, $data);
246
		}
247
	}
248
249
	/**
250
	 * insert meta data for a new file or folder
251
	 *
252
	 * @param string $file
253
	 * @param array $data
254
	 *
255
	 * @return int file id
256
	 * @throws \RuntimeException
257
	 *
258
	 * @suppress SqlInjectionChecker
259
	 */
260
	public function insert($file, array $data) {
261
		// normalize file
262
		$file = $this->normalize($file);
263
264
		if (isset($this->partial[$file])) { //add any saved partial data
265
			$data = array_merge($this->partial[$file], $data);
266
			unset($this->partial[$file]);
267
		}
268
269
		$requiredFields = ['size', 'mtime', 'mimetype'];
270
		foreach ($requiredFields as $field) {
271
			if (!isset($data[$field])) { //data not complete save as partial and return
272
				$this->partial[$file] = $data;
273
				return -1;
274
			}
275
		}
276
277
		$data['path'] = $file;
278
		$data['parent'] = $this->getParentId($file);
279
		$data['name'] = basename($file);
280
281
		[$values, $extensionValues] = $this->normalizeData($data);
282
		$values['storage'] = $this->getNumericStorageId();
283
284
		try {
285
			$builder = $this->connection->getQueryBuilder();
286
			$builder->insert('filecache');
287
288
			foreach ($values as $column => $value) {
289
				$builder->setValue($column, $builder->createNamedParameter($value));
290
			}
291
292
			if ($builder->execute()) {
293
				$fileId = $builder->getLastInsertId();
294
295
				if (count($extensionValues)) {
296
					$query = $this->getQueryBuilder();
297
					$query->insert('filecache_extended');
298
299
					$query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
300
					foreach ($extensionValues as $column => $value) {
301
						$query->setValue($column, $query->createNamedParameter($value));
302
					}
303
					$query->execute();
304
				}
305
306
				$this->eventDispatcher->dispatch(CacheInsertEvent::class, new CacheInsertEvent($this->storage, $file, $fileId));
0 ignored issues
show
Bug introduced by
OCP\Files\Cache\CacheInsertEvent::class of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

306
				$this->eventDispatcher->dispatch(/** @scrutinizer ignore-type */ CacheInsertEvent::class, new CacheInsertEvent($this->storage, $file, $fileId));
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new OCP\Files\Cache\Cach...torage, $file, $fileId). ( Ignorable by Annotation )

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

306
				$this->eventDispatcher->/** @scrutinizer ignore-call */ 
307
                            dispatch(CacheInsertEvent::class, new CacheInsertEvent($this->storage, $file, $fileId));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
307
				return $fileId;
308
			}
309
		} catch (UniqueConstraintViolationException $e) {
310
			// entry exists already
311
		}
312
313
		// The file was created in the mean time
314
		if (($id = $this->getId($file)) > -1) {
315
			$this->update($id, $data);
316
			return $id;
317
		} else {
318
			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.');
319
		}
320
	}
321
322
	/**
323
	 * update the metadata of an existing file or folder in the cache
324
	 *
325
	 * @param int $id the fileid of the existing file or folder
326
	 * @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
327
	 */
328
	public function update($id, array $data) {
329
330
		if (isset($data['path'])) {
331
			// normalize path
332
			$data['path'] = $this->normalize($data['path']);
333
		}
334
335
		if (isset($data['name'])) {
336
			// normalize path
337
			$data['name'] = $this->normalize($data['name']);
338
		}
339
340
		[$values, $extensionValues] = $this->normalizeData($data);
341
342
		if (count($values)) {
343
			$query = $this->getQueryBuilder();
344
345
			$query->update('filecache')
346
				->whereFileId($id)
347
				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
348
					return $query->expr()->orX(
349
						$query->expr()->neq($key, $query->createNamedParameter($value)),
350
						$query->expr()->isNull($key)
351
					);
352
				}, array_keys($values), array_values($values))));
353
354
			foreach ($values as $key => $value) {
355
				$query->set($key, $query->createNamedParameter($value));
356
			}
357
358
			$query->execute();
359
		}
360
361
		if (count($extensionValues)) {
362
			try {
363
				$query = $this->getQueryBuilder();
364
				$query->insert('filecache_extended');
365
366
				$query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
367
				foreach ($extensionValues as $column => $value) {
368
					$query->setValue($column, $query->createNamedParameter($value));
369
				}
370
371
				$query->execute();
372
			} catch (UniqueConstraintViolationException $e) {
373
				$query = $this->getQueryBuilder();
374
				$query->update('filecache_extended')
375
					->whereFileId($id)
376
					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
377
						return $query->expr()->orX(
378
							$query->expr()->neq($key, $query->createNamedParameter($value)),
379
							$query->expr()->isNull($key)
380
						);
381
					}, array_keys($extensionValues), array_values($extensionValues))));
382
383
				foreach ($extensionValues as $key => $value) {
384
					$query->set($key, $query->createNamedParameter($value));
385
				}
386
387
				$query->execute();
388
			}
389
		}
390
391
		$path = $this->getPathById($id);
392
		// path can still be null if the file doesn't exist
393
		if ($path !== null) {
394
			$this->eventDispatcher->dispatch(CacheUpdateEvent::class, new CacheUpdateEvent($this->storage, $path, $id));
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new OCP\Files\Cache\Cach...s->storage, $path, $id). ( Ignorable by Annotation )

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

394
			$this->eventDispatcher->/** @scrutinizer ignore-call */ 
395
                           dispatch(CacheUpdateEvent::class, new CacheUpdateEvent($this->storage, $path, $id));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
OCP\Files\Cache\CacheUpdateEvent::class of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

394
			$this->eventDispatcher->dispatch(/** @scrutinizer ignore-type */ CacheUpdateEvent::class, new CacheUpdateEvent($this->storage, $path, $id));
Loading history...
395
		}
396
	}
397
398
	/**
399
	 * extract query parts and params array from data array
400
	 *
401
	 * @param array $data
402
	 * @return array
403
	 */
404
	protected function normalizeData(array $data): array {
405
		$fields = [
406
			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
407
			'etag', 'permissions', 'checksum', 'storage'];
408
		$extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
409
410
		$doNotCopyStorageMTime = false;
411
		if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
412
			// this horrific magic tells it to not copy storage_mtime to mtime
413
			unset($data['mtime']);
414
			$doNotCopyStorageMTime = true;
415
		}
416
417
		$params = [];
418
		$extensionParams = [];
419
		foreach ($data as $name => $value) {
420
			if (array_search($name, $fields) !== false) {
421
				if ($name === 'path') {
422
					$params['path_hash'] = md5($value);
423
				} else if ($name === 'mimetype') {
424
					$params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
425
					$value = $this->mimetypeLoader->getId($value);
426
				} else if ($name === 'storage_mtime') {
427
					if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
428
						$params['mtime'] = $value;
429
					}
430
				} else if ($name === 'encrypted') {
431
					if (isset($data['encryptedVersion'])) {
432
						$value = $data['encryptedVersion'];
433
					} else {
434
						// Boolean to integer conversion
435
						$value = $value ? 1 : 0;
436
					}
437
				}
438
				$params[$name] = $value;
439
			}
440
			if (array_search($name, $extensionFields) !== false) {
441
				$extensionParams[$name] = $value;
442
			}
443
		}
444
		return [$params, array_filter($extensionParams)];
445
	}
446
447
	/**
448
	 * get the file id for a file
449
	 *
450
	 * 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
451
	 *
452
	 * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
453
	 *
454
	 * @param string $file
455
	 * @return int
456
	 */
457
	public function getId($file) {
458
		// normalize file
459
		$file = $this->normalize($file);
460
461
		$query = $this->getQueryBuilder();
462
		$query->select('fileid')
463
			->from('filecache')
464
			->whereStorageId()
465
			->wherePath($file);
466
467
		$id = $query->execute()->fetchColumn();
468
		return $id === false ? -1 : (int)$id;
469
	}
470
471
	/**
472
	 * get the id of the parent folder of a file
473
	 *
474
	 * @param string $file
475
	 * @return int
476
	 */
477
	public function getParentId($file) {
478
		if ($file === '') {
479
			return -1;
480
		} else {
481
			$parent = $this->getParentPath($file);
482
			return (int)$this->getId($parent);
483
		}
484
	}
485
486
	private function getParentPath($path) {
487
		$parent = dirname($path);
488
		if ($parent === '.') {
489
			$parent = '';
490
		}
491
		return $parent;
492
	}
493
494
	/**
495
	 * check if a file is available in the cache
496
	 *
497
	 * @param string $file
498
	 * @return bool
499
	 */
500
	public function inCache($file) {
501
		return $this->getId($file) != -1;
502
	}
503
504
	/**
505
	 * remove a file or folder from the cache
506
	 *
507
	 * when removing a folder from the cache all files and folders inside the folder will be removed as well
508
	 *
509
	 * @param string $file
510
	 */
511
	public function remove($file) {
512
		$entry = $this->get($file);
513
514
		if ($entry) {
515
			$query = $this->getQueryBuilder();
516
			$query->delete('filecache')
517
				->whereFileId($entry->getId());
518
			$query->execute();
519
520
			$query = $this->getQueryBuilder();
521
			$query->delete('filecache_extended')
522
				->whereFileId($entry->getId());
523
			$query->execute();
524
525
			if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
526
				$this->removeChildren($entry);
527
			}
528
		}
529
	}
530
531
	/**
532
	 * Get all sub folders of a folder
533
	 *
534
	 * @param ICacheEntry $entry the cache entry of the folder to get the subfolders for
535
	 * @return ICacheEntry[] the cache entries for the subfolders
536
	 */
537
	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...
538
		$children = $this->getFolderContentsById($entry->getId());
539
		return array_filter($children, function ($child) {
540
			return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
541
		});
542
	}
543
544
	/**
545
	 * Recursively remove all children of a folder
546
	 *
547
	 * @param ICacheEntry $entry the cache entry of the folder to remove the children of
548
	 * @throws \OC\DatabaseException
549
	 */
550
	private function removeChildren(ICacheEntry $entry) {
551
		$children = $this->getFolderContentsById($entry->getId());
552
		$childIds = array_map(function(ICacheEntry $cacheEntry) {
553
			return $cacheEntry->getId();
554
		}, $children);
555
		$childFolders = array_filter($children, function ($child) {
556
			return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
557
		});
558
		foreach ($childFolders as $folder) {
559
			$this->removeChildren($folder);
560
		}
561
562
		$query = $this->getQueryBuilder();
563
		$query->delete('filecache')
564
			->whereParent($entry->getId());
565
		$query->execute();
566
567
		$query = $this->getQueryBuilder();
568
		$query->delete('filecache_extended')
569
			->where($query->expr()->in('fileid', $query->createNamedParameter($childIds, IQueryBuilder::PARAM_INT_ARRAY)));
570
		$query->execute();
571
	}
572
573
	/**
574
	 * Move a file or folder in the cache
575
	 *
576
	 * @param string $source
577
	 * @param string $target
578
	 */
579
	public function move($source, $target) {
580
		$this->moveFromCache($this, $source, $target);
581
	}
582
583
	/**
584
	 * Get the storage id and path needed for a move
585
	 *
586
	 * @param string $path
587
	 * @return array [$storageId, $internalPath]
588
	 */
589
	protected function getMoveInfo($path) {
590
		return [$this->getNumericStorageId(), $path];
591
	}
592
593
	/**
594
	 * Move a file or folder in the cache
595
	 *
596
	 * @param \OCP\Files\Cache\ICache $sourceCache
597
	 * @param string $sourcePath
598
	 * @param string $targetPath
599
	 * @throws \OC\DatabaseException
600
	 * @throws \Exception if the given storages have an invalid id
601
	 * @suppress SqlInjectionChecker
602
	 */
603
	public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
604
		if ($sourceCache instanceof Cache) {
605
			// normalize source and target
606
			$sourcePath = $this->normalize($sourcePath);
607
			$targetPath = $this->normalize($targetPath);
608
609
			$sourceData = $sourceCache->get($sourcePath);
610
			$sourceId = $sourceData['fileid'];
611
			$newParentId = $this->getParentId($targetPath);
612
613
			list($sourceStorageId, $sourcePath) = $sourceCache->getMoveInfo($sourcePath);
614
			list($targetStorageId, $targetPath) = $this->getMoveInfo($targetPath);
615
616
			if (is_null($sourceStorageId) || $sourceStorageId === false) {
0 ignored issues
show
introduced by
The condition $sourceStorageId === false is always false.
Loading history...
617
				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
618
			}
619
			if (is_null($targetStorageId) || $targetStorageId === false) {
0 ignored issues
show
introduced by
The condition $targetStorageId === false is always false.
Loading history...
620
				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
621
			}
622
623
			$this->connection->beginTransaction();
624
			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
625
				//update all child entries
626
				$sourceLength = mb_strlen($sourcePath);
627
				$query = $this->connection->getQueryBuilder();
628
629
				$fun = $query->func();
630
				$newPathFunction = $fun->concat(
631
					$query->createNamedParameter($targetPath),
632
					$fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
633
				);
634
				$query->update('filecache')
635
					->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
636
					->set('path_hash', $fun->md5($newPathFunction))
637
					->set('path', $newPathFunction)
638
					->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
639
					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
640
641
				try {
642
					$query->execute();
643
				} catch (\OC\DatabaseException $e) {
644
					$this->connection->rollBack();
645
					throw $e;
646
				}
647
			}
648
649
			$query = $this->getQueryBuilder();
650
			$query->update('filecache')
651
				->set('storage', $query->createNamedParameter($targetStorageId))
652
				->set('path', $query->createNamedParameter($targetPath))
653
				->set('path_hash', $query->createNamedParameter(md5($targetPath)))
654
				->set('name', $query->createNamedParameter(basename($targetPath)))
655
				->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
656
				->whereFileId($sourceId);
657
			$query->execute();
658
659
			$this->connection->commit();
660
		} else {
661
			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
662
		}
663
	}
664
665
	/**
666
	 * remove all entries for files that are stored on the storage from the cache
667
	 */
668
	public function clear() {
669
		$query = $this->getQueryBuilder();
670
		$query->delete('filecache')
671
			->whereStorageId();
672
		$query->execute();
673
674
		$query = $this->connection->getQueryBuilder();
675
		$query->delete('storages')
676
			->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
677
		$query->execute();
678
	}
679
680
	/**
681
	 * Get the scan status of a file
682
	 *
683
	 * - Cache::NOT_FOUND: File is not in the cache
684
	 * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
685
	 * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
686
	 * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
687
	 *
688
	 * @param string $file
689
	 *
690
	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
691
	 */
692
	public function getStatus($file) {
693
		// normalize file
694
		$file = $this->normalize($file);
695
696
		$query = $this->getQueryBuilder();
697
		$query->select('size')
698
			->from('filecache')
699
			->whereStorageId()
700
			->wherePath($file);
701
		$size = $query->execute()->fetchColumn();
702
		if ($size !== false) {
703
			if ((int)$size === -1) {
704
				return self::SHALLOW;
705
			} else {
706
				return self::COMPLETE;
707
			}
708
		} else {
709
			if (isset($this->partial[$file])) {
710
				return self::PARTIAL;
711
			} else {
712
				return self::NOT_FOUND;
713
			}
714
		}
715
	}
716
717
	/**
718
	 * search for files matching $pattern
719
	 *
720
	 * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
721
	 * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
722
	 */
723
	public function search($pattern) {
724
		// normalize pattern
725
		$pattern = $this->normalize($pattern);
726
727
		if ($pattern === '%%') {
728
			return [];
729
		}
730
731
		$query = $this->getQueryBuilder();
732
		$query->selectFileCache()
733
			->whereStorageId()
734
			->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
735
736
		return array_map(function (array $data) {
737
			return self::cacheEntryFromData($data, $this->mimetypeLoader);
738
		}, $query->execute()->fetchAll());
739
	}
740
741
	/**
742
	 * @param Statement $result
743
	 * @return CacheEntry[]
744
	 */
745
	private function searchResultToCacheEntries(Statement $result) {
746
		$files = $result->fetchAll();
747
748
		return array_map(function (array $data) {
749
			return self::cacheEntryFromData($data, $this->mimetypeLoader);
750
		}, $files);
751
	}
752
753
	/**
754
	 * search for files by mimetype
755
	 *
756
	 * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
757
	 *        where it will search for all mimetypes in the group ('image/*')
758
	 * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
759
	 */
760
	public function searchByMime($mimetype) {
761
		$mimeId = $this->mimetypeLoader->getId($mimetype);
762
763
		$query = $this->getQueryBuilder();
764
		$query->selectFileCache()
765
			->whereStorageId();
766
767
		if (strpos($mimetype, '/')) {
768
			$query->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
769
		} else {
770
			$query->andWhere($query->expr()->eq('mimepart', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
771
		}
772
773
		return array_map(function (array $data) {
774
			return self::cacheEntryFromData($data, $this->mimetypeLoader);
775
		}, $query->execute()->fetchAll());
776
	}
777
778
	public function searchQuery(ISearchQuery $searchQuery) {
779
		$builder = $this->getQueryBuilder();
780
781
		$query = $builder->selectFileCache('file');
782
783
		$query->whereStorageId();
784
785
		if ($this->querySearchHelper->shouldJoinTags($searchQuery->getSearchOperation())) {
786
			$query
787
				->innerJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid'))
788
				->innerJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX(
789
					$builder->expr()->eq('tagmap.type', 'tag.type'),
790
					$builder->expr()->eq('tagmap.categoryid', 'tag.id')
791
				))
792
				->andWhere($builder->expr()->eq('tag.type', $builder->createNamedParameter('files')))
793
				->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($searchQuery->getUser()->getUID())));
794
		}
795
796
		$query->andWhere($this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation()));
797
798
		$this->querySearchHelper->addSearchOrdersToQuery($query, $searchQuery->getOrder());
799
800
		if ($searchQuery->getLimit()) {
801
			$query->setMaxResults($searchQuery->getLimit());
802
		}
803
		if ($searchQuery->getOffset()) {
804
			$query->setFirstResult($searchQuery->getOffset());
805
		}
806
807
		$result = $query->execute();
808
		return $this->searchResultToCacheEntries($result);
809
	}
810
811
	/**
812
	 * Re-calculate the folder size and the size of all parent folders
813
	 *
814
	 * @param string|boolean $path
815
	 * @param array $data (optional) meta data of the folder
816
	 */
817
	public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
818
		$this->calculateFolderSize($path, $data);
819
		if ($path !== '') {
820
			$parent = dirname($path);
821
			if ($parent === '.' or $parent === '/') {
822
				$parent = '';
823
			}
824
			if ($isBackgroundScan) {
825
				$parentData = $this->get($parent);
826
				if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
827
					$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

827
					$this->correctFolderSize($parent, /** @scrutinizer ignore-type */ $parentData, $isBackgroundScan);
Loading history...
828
				}
829
			} else {
830
				$this->correctFolderSize($parent);
831
			}
832
		}
833
	}
834
835
	/**
836
	 * get the incomplete count that shares parent $folder
837
	 *
838
	 * @param int $fileId the file id of the folder
839
	 * @return int
840
	 */
841
	public function getIncompleteChildrenCount($fileId) {
842
		if ($fileId > -1) {
843
			$query = $this->getQueryBuilder();
844
			$query->select($query->func()->count())
845
				->from('filecache')
846
				->whereParent($fileId)
847
				->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
848
849
			return (int)$query->execute()->fetchColumn();
850
		}
851
		return -1;
852
	}
853
854
	/**
855
	 * calculate the size of a folder and set it in the cache
856
	 *
857
	 * @param string $path
858
	 * @param array $entry (optional) meta data of the folder
859
	 * @return int
860
	 */
861
	public function calculateFolderSize($path, $entry = null) {
862
		$totalSize = 0;
863
		if (is_null($entry) or !isset($entry['fileid'])) {
864
			$entry = $this->get($path);
865
		}
866
		if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
867
			$id = $entry['fileid'];
868
869
			$query = $this->getQueryBuilder();
870
			$query->selectAlias($query->func()->sum('size'), 'f1')
871
				->selectAlias($query->func()->min('size'), 'f2')
872
				->from('filecache')
873
				->whereStorageId()
874
				->whereParent($id);
875
876
			if ($row = $query->execute()->fetch()) {
877
				list($sum, $min) = array_values($row);
878
				$sum = 0 + $sum;
879
				$min = 0 + $min;
880
				if ($min === -1) {
881
					$totalSize = $min;
882
				} else {
883
					$totalSize = $sum;
884
				}
885
				if ($entry['size'] !== $totalSize) {
886
					$this->update($id, ['size' => $totalSize]);
887
				}
888
			}
889
		}
890
		return $totalSize;
891
	}
892
893
	/**
894
	 * get all file ids on the files on the storage
895
	 *
896
	 * @return int[]
897
	 */
898
	public function getAll() {
899
		$query = $this->getQueryBuilder();
900
		$query->select('fileid')
901
			->from('filecache')
902
			->whereStorageId();
903
904
		return array_map(function ($id) {
905
			return (int)$id;
906
		}, $query->execute()->fetchAll(\PDO::FETCH_COLUMN));
907
	}
908
909
	/**
910
	 * find a folder in the cache which has not been fully scanned
911
	 *
912
	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
913
	 * use the one with the highest id gives the best result with the background scanner, since that is most
914
	 * likely the folder where we stopped scanning previously
915
	 *
916
	 * @return string|bool the path of the folder or false when no folder matched
917
	 */
918
	public function getIncomplete() {
919
		$query = $this->getQueryBuilder();
920
		$query->select('path')
921
			->from('filecache')
922
			->whereStorageId()
923
			->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
924
			->orderBy('fileid', 'DESC');
925
926
		return $query->execute()->fetchColumn();
927
	}
928
929
	/**
930
	 * get the path of a file on this storage by it's file id
931
	 *
932
	 * @param int $id the file id of the file or folder to search
933
	 * @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
934
	 */
935
	public function getPathById($id) {
936
		$query = $this->getQueryBuilder();
937
		$query->select('path')
938
			->from('filecache')
939
			->whereStorageId()
940
			->whereFileId($id);
941
942
		$path = $query->execute()->fetchColumn();
943
		return $path === false ? null : $path;
944
	}
945
946
	/**
947
	 * get the storage id of the storage for a file and the internal path of the file
948
	 * unlike getPathById this does not limit the search to files on this storage and
949
	 * instead does a global search in the cache table
950
	 *
951
	 * @param int $id
952
	 * @return array first element holding the storage id, second the path
953
	 * @deprecated use getPathById() instead
954
	 */
955
	static public function getById($id) {
956
		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
957
		$query->select('path', 'storage')
958
			->from('filecache')
959
			->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
960
		if ($row = $query->execute()->fetch()) {
961
			$numericId = $row['storage'];
962
			$path = $row['path'];
963
		} else {
964
			return null;
965
		}
966
967
		if ($id = Storage::getStorageId($numericId)) {
968
			return [$id, $path];
969
		} else {
970
			return null;
971
		}
972
	}
973
974
	/**
975
	 * normalize the given path
976
	 *
977
	 * @param string $path
978
	 * @return string
979
	 */
980
	public function normalize($path) {
981
982
		return trim(\OC_Util::normalizeUnicode($path), '/');
983
	}
984
}
985