Passed
Push — master ( 7042f9...a688f4 )
by Roeland
21:39 queued 10:37
created

Cache::insert()   F

Complexity

Conditions 12
Paths 970

Size

Total Lines 65
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 40
c 1
b 0
f 0
nc 970
nop 2
dl 0
loc 65
rs 2.8416

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Andreas Fischer <[email protected]>
6
 * @author Ari Selseng <[email protected]>
7
 * @author Artem Kochnev <[email protected]>
8
 * @author Björn Schießle <[email protected]>
9
 * @author 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 Vincent Petry <[email protected]>
21
 *
22
 * @license AGPL-3.0
23
 *
24
 * This code is free software: you can redistribute it and/or modify
25
 * it under the terms of the GNU Affero General Public License, version 3,
26
 * as published by the Free Software Foundation.
27
 *
28
 * This program is distributed in the hope that it will be useful,
29
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31
 * GNU Affero General Public License for more details.
32
 *
33
 * You should have received a copy of the GNU Affero General Public License, version 3,
34
 * along with this program. If not, see <http://www.gnu.org/licenses/>
35
 *
36
 */
37
38
namespace OC\Files\Cache;
39
40
use Doctrine\DBAL\Driver\Statement;
41
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
42
use OCP\DB\QueryBuilder\IQueryBuilder;
43
use OCP\Files\Cache\CacheInsertEvent;
44
use OCP\Files\Cache\CacheUpdateEvent;
45
use OCP\Files\Cache\ICache;
46
use OCP\Files\Cache\ICacheEntry;
47
use OCP\Files\FileInfo;
48
use OCP\Files\IMimeTypeLoader;
49
use OCP\Files\Search\ISearchQuery;
50
use OCP\Files\Storage\IStorage;
51
use OCP\IDBConnection;
52
53
/**
54
 * Metadata cache for a storage
55
 *
56
 * The cache stores the metadata for all files and folders in a storage and is kept up to date trough the following mechanisms:
57
 *
58
 * - Scanner: scans the storage and updates the cache where needed
59
 * - Watcher: checks for changes made to the filesystem outside of the ownCloud instance and rescans files and folder when a change is detected
60
 * - Updater: listens to changes made to the filesystem inside of the ownCloud instance and updates the cache where needed
61
 * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
62
 */
63
class Cache implements ICache {
64
	use MoveFromCacheTrait {
65
		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
66
	}
67
68
	/**
69
	 * @var array partial data for the cache
70
	 */
71
	protected $partial = [];
72
73
	/**
74
	 * @var string
75
	 */
76
	protected $storageId;
77
78
	private $storage;
79
80
	/**
81
	 * @var Storage $storageCache
82
	 */
83
	protected $storageCache;
84
85
	/** @var IMimeTypeLoader */
86
	protected $mimetypeLoader;
87
88
	/**
89
	 * @var IDBConnection
90
	 */
91
	protected $connection;
92
93
	protected $eventDispatcher;
94
95
	/** @var QuerySearchHelper */
96
	protected $querySearchHelper;
97
98
	/**
99
	 * @param IStorage $storage
100
	 */
101
	public function __construct(IStorage $storage) {
102
		$this->storageId = $storage->getId();
103
		$this->storage = $storage;
104
		if (strlen($this->storageId) > 64) {
105
			$this->storageId = md5($this->storageId);
106
		}
107
108
		$this->storageCache = new Storage($storage);
109
		$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
110
		$this->connection = \OC::$server->getDatabaseConnection();
111
		$this->eventDispatcher = \OC::$server->getEventDispatcher();
0 ignored issues
show
Deprecated Code introduced by
The function OC\Server::getEventDispatcher() has been deprecated: 18.0.0 use \OCP\EventDispatcher\IEventDispatcher ( Ignorable by Annotation )

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

111
		$this->eventDispatcher = /** @scrutinizer ignore-deprecated */ \OC::$server->getEventDispatcher();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
112
		$this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
113
	}
114
115
	private function getQueryBuilder() {
116
		return new CacheQueryBuilder(
117
			$this->connection,
118
			\OC::$server->getSystemConfig(),
119
			\OC::$server->getLogger(),
120
			$this
121
		);
122
	}
123
124
	/**
125
	 * Get the numeric storage id for this cache's storage
126
	 *
127
	 * @return int
128
	 */
129
	public function getNumericStorageId() {
130
		return $this->storageCache->getNumericId();
131
	}
132
133
	/**
134
	 * get the stored metadata of a file or folder
135
	 *
136
	 * @param string | int $file either the path of a file or folder or the file id for a file or folder
137
	 * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
138
	 */
139
	public function get($file) {
140
		$query = $this->getQueryBuilder();
141
		$query->selectFileCache();
142
143
		if (is_string($file) or $file == '') {
144
			// normalize file
145
			$file = $this->normalize($file);
146
147
			$query->whereStorageId()
148
				->wherePath($file);
149
		} else { //file id
150
			$query->whereFileId($file);
151
		}
152
153
		$data = $query->execute()->fetch();
154
155
		//merge partial data
156
		if (!$data and is_string($file) and isset($this->partial[$file])) {
157
			return $this->partial[$file];
158
		} else if (!$data) {
159
			return $data;
160
		} else {
161
			return self::cacheEntryFromData($data, $this->mimetypeLoader);
162
		}
163
	}
164
165
	/**
166
	 * Create a CacheEntry from database row
167
	 *
168
	 * @param array $data
169
	 * @param IMimeTypeLoader $mimetypeLoader
170
	 * @return CacheEntry
171
	 */
172
	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
173
		//fix types
174
		$data['fileid'] = (int)$data['fileid'];
175
		$data['parent'] = (int)$data['parent'];
176
		$data['size'] = 0 + $data['size'];
177
		$data['mtime'] = (int)$data['mtime'];
178
		$data['storage_mtime'] = (int)$data['storage_mtime'];
179
		$data['encryptedVersion'] = (int)$data['encrypted'];
180
		$data['encrypted'] = (bool)$data['encrypted'];
181
		$data['storage_id'] = $data['storage'];
182
		$data['storage'] = (int)$data['storage'];
183
		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
184
		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
185
		if ($data['storage_mtime'] == 0) {
186
			$data['storage_mtime'] = $data['mtime'];
187
		}
188
		$data['permissions'] = (int)$data['permissions'];
189
		if (isset($data['creation_time'])) {
190
			$data['creation_time'] = (int) $data['creation_time'];
191
		}
192
		if (isset($data['upload_time'])) {
193
			$data['upload_time'] = (int) $data['upload_time'];
194
		}
195
		return new CacheEntry($data);
196
	}
197
198
	/**
199
	 * get the metadata of all files stored in $folder
200
	 *
201
	 * @param string $folder
202
	 * @return ICacheEntry[]
203
	 */
204
	public function getFolderContents($folder) {
205
		$fileId = $this->getId($folder);
206
		return $this->getFolderContentsById($fileId);
207
	}
208
209
	/**
210
	 * get the metadata of all files stored in $folder
211
	 *
212
	 * @param int $fileId the file id of the folder
213
	 * @return ICacheEntry[]
214
	 */
215
	public function getFolderContentsById($fileId) {
216
		if ($fileId > -1) {
217
			$query = $this->getQueryBuilder();
218
			$query->selectFileCache()
219
				->whereParent($fileId)
220
				->orderBy('name', 'ASC');
221
222
			$files = $query->execute()->fetchAll();
223
			return array_map(function (array $data) {
224
				return self::cacheEntryFromData($data, $this->mimetypeLoader);
225
			}, $files);
226
		}
227
		return [];
228
	}
229
230
	/**
231
	 * insert or update meta data for a file or folder
232
	 *
233
	 * @param string $file
234
	 * @param array $data
235
	 *
236
	 * @return int file id
237
	 * @throws \RuntimeException
238
	 */
239
	public function put($file, array $data) {
240
		if (($id = $this->getId($file)) > -1) {
241
			$this->update($id, $data);
242
			return $id;
243
		} else {
244
			return $this->insert($file, $data);
245
		}
246
	}
247
248
	/**
249
	 * insert meta data for a new file or folder
250
	 *
251
	 * @param string $file
252
	 * @param array $data
253
	 *
254
	 * @return int file id
255
	 * @throws \RuntimeException
256
	 *
257
	 * @suppress SqlInjectionChecker
258
	 */
259
	public function insert($file, array $data) {
260
		// normalize file
261
		$file = $this->normalize($file);
262
263
		if (isset($this->partial[$file])) { //add any saved partial data
264
			$data = array_merge($this->partial[$file], $data);
265
			unset($this->partial[$file]);
266
		}
267
268
		$requiredFields = ['size', 'mtime', 'mimetype'];
269
		foreach ($requiredFields as $field) {
270
			if (!isset($data[$field])) { //data not complete save as partial and return
271
				$this->partial[$file] = $data;
272
				return -1;
273
			}
274
		}
275
276
		$data['path'] = $file;
277
		if (!isset($data['parent'])) {
278
			$data['parent'] = $this->getParentId($file);
279
		}
280
		$data['name'] = basename($file);
281
282
		[$values, $extensionValues] = $this->normalizeData($data);
283
		$values['storage'] = $this->getNumericStorageId();
284
285
		try {
286
			$builder = $this->connection->getQueryBuilder();
287
			$builder->insert('filecache');
288
289
			foreach ($values as $column => $value) {
290
				$builder->setValue($column, $builder->createNamedParameter($value));
291
			}
292
293
			if ($builder->execute()) {
294
				$fileId = $builder->getLastInsertId();
295
296
				if (count($extensionValues)) {
297
					$query = $this->getQueryBuilder();
298
					$query->insert('filecache_extended');
299
300
					$query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
301
					foreach ($extensionValues as $column => $value) {
302
						$query->setValue($column, $query->createNamedParameter($value));
303
					}
304
					$query->execute();
305
				}
306
307
				$this->eventDispatcher->dispatch(CacheInsertEvent::class, new CacheInsertEvent($this->storage, $file, $fileId));
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...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

307
				$this->eventDispatcher->/** @scrutinizer ignore-call */ 
308
                            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...
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

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

399
			$this->eventDispatcher->dispatch(/** @scrutinizer ignore-type */ CacheUpdateEvent::class, new CacheUpdateEvent($this->storage, $path, $id));
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...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

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

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