Completed
Pull Request — master (#3089)
by Robin
30:27 queued 16:11
created

Cache::getSubFolders()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 6
rs 9.4285
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 Björn Schießle <[email protected]>
7
 * @author Florin Peter <[email protected]>
8
 * @author Jens-Christian Fischer <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Jörn Friedrich Dreyer <[email protected]>
11
 * @author Lukas Reschke <[email protected]>
12
 * @author Michael Gapczynski <[email protected]>
13
 * @author Morris Jobke <[email protected]>
14
 * @author Robin Appelman <[email protected]>
15
 * @author Robin McCorkell <[email protected]>
16
 * @author Roeland Jago Douma <[email protected]>
17
 * @author TheSFReader <[email protected]>
18
 * @author Thomas Müller <[email protected]>
19
 * @author Vincent Petry <[email protected]>
20
 * @author Xuanwo <[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 OCP\DB\QueryBuilder\IQueryBuilder;
41
use Doctrine\DBAL\Driver\Statement;
42
use OCP\Files\Cache\ICache;
43
use OCP\Files\Cache\ICacheEntry;
44
use \OCP\Files\IMimeTypeLoader;
45
use OCP\Files\Search\ISearchQuery;
46
use OCP\IDBConnection;
47
48
/**
49
 * Metadata cache for a storage
50
 *
51
 * The cache stores the metadata for all files and folders in a storage and is kept up to date trough the following mechanisms:
52
 *
53
 * - Scanner: scans the storage and updates the cache where needed
54
 * - Watcher: checks for changes made to the filesystem outside of the ownCloud instance and rescans files and folder when a change is detected
55
 * - Updater: listens to changes made to the filesystem inside of the ownCloud instance and updates the cache where needed
56
 * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
57
 */
58
class Cache implements ICache {
59
	use MoveFromCacheTrait {
60
		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
61
	}
62
63
	/**
64
	 * @var array partial data for the cache
65
	 */
66
	protected $partial = array();
67
68
	/**
69
	 * @var string
70
	 */
71
	protected $storageId;
72
73
	/**
74
	 * @var Storage $storageCache
75
	 */
76
	protected $storageCache;
77
78
	/** @var IMimeTypeLoader */
79
	protected $mimetypeLoader;
80
81
	/**
82
	 * @var IDBConnection
83
	 */
84
	protected $connection;
85
86
	/** @var QuerySearchHelper */
87
	protected $querySearchHelper;
88
89
	/**
90
	 * @param \OC\Files\Storage\Storage|string $storage
91
	 */
92
	public function __construct($storage) {
93
		if ($storage instanceof \OC\Files\Storage\Storage) {
94
			$this->storageId = $storage->getId();
95
		} else {
96
			$this->storageId = $storage;
97
		}
98
		if (strlen($this->storageId) > 64) {
99
			$this->storageId = md5($this->storageId);
100
		}
101
102
		$this->storageCache = new Storage($storage);
103
		$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
104
		$this->connection = \OC::$server->getDatabaseConnection();
105
		$this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
106
	}
107
108
	/**
109
	 * Get the numeric storage id for this cache's storage
110
	 *
111
	 * @return int
112
	 */
113
	public function getNumericStorageId() {
114
		return $this->storageCache->getNumericId();
115
	}
116
117
	/**
118
	 * get the stored metadata of a file or folder
119
	 *
120
	 * @param string | int $file either the path of a file or folder or the file id for a file or folder
121
	 * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
122
	 */
123
	public function get($file) {
124
		if (is_string($file) or $file == '') {
125
			// normalize file
126
			$file = $this->normalize($file);
127
128
			$where = 'WHERE `storage` = ? AND `path_hash` = ?';
129
			$params = array($this->getNumericStorageId(), md5($file));
130
		} else { //file id
131
			$where = 'WHERE `fileid` = ?';
132
			$params = array($file);
133
		}
134
		$sql = 'SELECT `fileid`, `storage`, `path`, `path_hash`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`,
135
					   `storage_mtime`, `encrypted`, `etag`, `permissions`, `checksum`
136
				FROM `*PREFIX*filecache` ' . $where;
137
		$result = $this->connection->executeQuery($sql, $params);
138
		$data = $result->fetch();
139
140
		//FIXME hide this HACK in the next database layer, or just use doctrine and get rid of MDB2 and PDO
141
		//PDO returns false, MDB2 returns null, oracle always uses MDB2, so convert null to false
142
		if ($data === null) {
143
			$data = false;
144
		}
145
146
		//merge partial data
147
		if (!$data and is_string($file)) {
148
			if (isset($this->partial[$file])) {
149
				$data = $this->partial[$file];
150
			}
151
			return $data;
152
		} else {
153
			return self::cacheEntryFromData($data, $this->mimetypeLoader);
154
		}
155
	}
156
157
	/**
158
	 * Create a CacheEntry from database row
159
	 *
160
	 * @param array $data
161
	 * @param IMimeTypeLoader $mimetypeLoader
162
	 * @return CacheEntry
163
	 */
164
	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
165
		//fix types
166
		$data['fileid'] = (int)$data['fileid'];
167
		$data['parent'] = (int)$data['parent'];
168
		$data['size'] = 0 + $data['size'];
169
		$data['mtime'] = (int)$data['mtime'];
170
		$data['storage_mtime'] = (int)$data['storage_mtime'];
171
		$data['encryptedVersion'] = (int)$data['encrypted'];
172
		$data['encrypted'] = (bool)$data['encrypted'];
173
		$data['storage_id'] = $data['storage'];
174
		$data['storage'] = (int)$data['storage'];
175
		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
176
		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
177
		if ($data['storage_mtime'] == 0) {
178
			$data['storage_mtime'] = $data['mtime'];
179
		}
180
		$data['permissions'] = (int)$data['permissions'];
181
		return new CacheEntry($data);
182
	}
183
184
	/**
185
	 * get the metadata of all files stored in $folder
186
	 *
187
	 * @param string $folder
188
	 * @return ICacheEntry[]
189
	 */
190
	public function getFolderContents($folder) {
191
		$fileId = $this->getId($folder);
192
		return $this->getFolderContentsById($fileId);
193
	}
194
195
	/**
196
	 * get the metadata of all files stored in $folder
197
	 *
198
	 * @param int $fileId the file id of the folder
199
	 * @return ICacheEntry[]
200
	 */
201
	public function getFolderContentsById($fileId) {
202
		if ($fileId > -1) {
203
			$sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`,
204
						   `storage_mtime`, `encrypted`, `etag`, `permissions`, `checksum`
205
					FROM `*PREFIX*filecache` WHERE `parent` = ? ORDER BY `name` ASC';
206
			$result = $this->connection->executeQuery($sql, [$fileId]);
207
			$files = $result->fetchAll();
208
			return array_map(function (array $data) {
209
				return self::cacheEntryFromData($data, $this->mimetypeLoader);;
210
			}, $files);
211
		} else {
212
			return array();
213
		}
214
	}
215
216
	/**
217
	 * insert or update meta data for a file or folder
218
	 *
219
	 * @param string $file
220
	 * @param array $data
221
	 *
222
	 * @return int file id
223
	 * @throws \RuntimeException
224
	 */
225 View Code Duplication
	public function put($file, array $data) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
226
		if (($id = $this->getId($file)) > -1) {
227
			$this->update($id, $data);
228
			return $id;
229
		} else {
230
			return $this->insert($file, $data);
231
		}
232
	}
233
234
	/**
235
	 * insert meta data for a new file or folder
236
	 *
237
	 * @param string $file
238
	 * @param array $data
239
	 *
240
	 * @return int file id
241
	 * @throws \RuntimeException
242
	 */
243
	public function insert($file, array $data) {
244
		// normalize file
245
		$file = $this->normalize($file);
246
247
		if (isset($this->partial[$file])) { //add any saved partial data
248
			$data = array_merge($this->partial[$file], $data);
249
			unset($this->partial[$file]);
250
		}
251
252
		$requiredFields = array('size', 'mtime', 'mimetype');
253
		foreach ($requiredFields as $field) {
254
			if (!isset($data[$field])) { //data not complete save as partial and return
255
				$this->partial[$file] = $data;
256
				return -1;
257
			}
258
		}
259
260
		$data['path'] = $file;
261
		$data['parent'] = $this->getParentId($file);
262
		$data['name'] = \OC_Util::basename($file);
263
264
		list($queryParts, $params) = $this->buildParts($data);
265
		$queryParts[] = '`storage`';
266
		$params[] = $this->getNumericStorageId();
267
268
		$queryParts = array_map(function ($item) {
269
			return trim($item, "`");
270
		}, $queryParts);
271
		$values = array_combine($queryParts, $params);
272
		if (\OC::$server->getDatabaseConnection()->insertIfNotExist('*PREFIX*filecache', $values, [
273
			'storage',
274
			'path_hash',
275
		])
276
		) {
277
			return (int)$this->connection->lastInsertId('*PREFIX*filecache');
278
		}
279
280
		// The file was created in the mean time
281
		if (($id = $this->getId($file)) > -1) {
282
			$this->update($id, $data);
283
			return $id;
284
		} else {
285
			throw new \RuntimeException('File entry could not be inserted with insertIfNotExist() but could also not be selected with getId() in order to perform an update. Please try again.');
286
		}
287
	}
288
289
	/**
290
	 * update the metadata of an existing file or folder in the cache
291
	 *
292
	 * @param int $id the fileid of the existing file or folder
293
	 * @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
294
	 */
295
	public function update($id, array $data) {
296
297
		if (isset($data['path'])) {
298
			// normalize path
299
			$data['path'] = $this->normalize($data['path']);
300
		}
301
302
		if (isset($data['name'])) {
303
			// normalize path
304
			$data['name'] = $this->normalize($data['name']);
305
		}
306
307
		list($queryParts, $params) = $this->buildParts($data);
308
		// duplicate $params because we need the parts twice in the SQL statement
309
		// once for the SET part, once in the WHERE clause
310
		$params = array_merge($params, $params);
311
		$params[] = $id;
312
313
		// don't update if the data we try to set is the same as the one in the record
314
		// some databases (Postgres) don't like superfluous updates
315
		$sql = 'UPDATE `*PREFIX*filecache` SET ' . implode(' = ?, ', $queryParts) . '=? ' .
316
			'WHERE (' .
317
			implode(' <> ? OR ', $queryParts) . ' <> ? OR ' .
318
			implode(' IS NULL OR ', $queryParts) . ' IS NULL' .
319
			') AND `fileid` = ? ';
320
		$this->connection->executeQuery($sql, $params);
321
322
	}
323
324
	/**
325
	 * extract query parts and params array from data array
326
	 *
327
	 * @param array $data
328
	 * @return array [$queryParts, $params]
329
	 *        $queryParts: string[], the (escaped) column names to be set in the query
330
	 *        $params: mixed[], the new values for the columns, to be passed as params to the query
331
	 */
332
	protected function buildParts(array $data) {
333
		$fields = array(
334
			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
335
			'etag', 'permissions', 'checksum');
336
337
		$doNotCopyStorageMTime = false;
338
		if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
339
			// this horrific magic tells it to not copy storage_mtime to mtime
340
			unset($data['mtime']);
341
			$doNotCopyStorageMTime = true;
342
		}
343
344
		$params = array();
345
		$queryParts = array();
346
		foreach ($data as $name => $value) {
347
			if (array_search($name, $fields) !== false) {
348
				if ($name === 'path') {
349
					$params[] = md5($value);
350
					$queryParts[] = '`path_hash`';
351
				} elseif ($name === 'mimetype') {
352
					$params[] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
353
					$queryParts[] = '`mimepart`';
354
					$value = $this->mimetypeLoader->getId($value);
355
				} elseif ($name === 'storage_mtime') {
356
					if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
357
						$params[] = $value;
358
						$queryParts[] = '`mtime`';
359
					}
360
				} elseif ($name === 'encrypted') {
361
					if (isset($data['encryptedVersion'])) {
362
						$value = $data['encryptedVersion'];
363
					} else {
364
						// Boolean to integer conversion
365
						$value = $value ? 1 : 0;
366
					}
367
				}
368
				$params[] = $value;
369
				$queryParts[] = '`' . $name . '`';
370
			}
371
		}
372
		return array($queryParts, $params);
373
	}
374
375
	/**
376
	 * get the file id for a file
377
	 *
378
	 * 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
379
	 *
380
	 * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
381
	 *
382
	 * @param string $file
383
	 * @return int
384
	 */
385
	public function getId($file) {
386
		// normalize file
387
		$file = $this->normalize($file);
388
389
		$pathHash = md5($file);
390
391
		$sql = 'SELECT `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path_hash` = ?';
392
		$result = $this->connection->executeQuery($sql, array($this->getNumericStorageId(), $pathHash));
393
		if ($row = $result->fetch()) {
394
			return $row['fileid'];
395
		} else {
396
			return -1;
397
		}
398
	}
399
400
	/**
401
	 * get the id of the parent folder of a file
402
	 *
403
	 * @param string $file
404
	 * @return int
405
	 */
406
	public function getParentId($file) {
407
		if ($file === '') {
408
			return -1;
409
		} else {
410
			$parent = $this->getParentPath($file);
411
			return (int)$this->getId($parent);
412
		}
413
	}
414
415
	private function getParentPath($path) {
416
		$parent = dirname($path);
417
		if ($parent === '.') {
418
			$parent = '';
419
		}
420
		return $parent;
421
	}
422
423
	/**
424
	 * check if a file is available in the cache
425
	 *
426
	 * @param string $file
427
	 * @return bool
428
	 */
429
	public function inCache($file) {
430
		return $this->getId($file) != -1;
431
	}
432
433
	/**
434
	 * remove a file or folder from the cache
435
	 *
436
	 * when removing a folder from the cache all files and folders inside the folder will be removed as well
437
	 *
438
	 * @param string $file
439
	 */
440 View Code Duplication
	public function remove($file) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
441
		$entry = $this->get($file);
442
		$sql = 'DELETE FROM `*PREFIX*filecache` WHERE `fileid` = ?';
443
		$this->connection->executeQuery($sql, array($entry['fileid']));
444
		if ($entry['mimetype'] === 'httpd/unix-directory') {
445
			$this->removeChildren($entry);
0 ignored issues
show
Documentation introduced by
$entry is of type object<OCP\Files\Cache\ICacheEntry>|false, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
446
		}
447
	}
448
449
	/**
450
	 * Get all sub folders of a folder
451
	 *
452
	 * @param array $entry the cache entry of the folder to get the subfolders for
453
	 * @return array[] the cache entries for the subfolders
454
	 */
455
	private function getSubFolders($entry) {
456
		$children = $this->getFolderContentsById($entry['fileid']);
457
		return array_filter($children, function ($child) {
458
			return $child['mimetype'] === 'httpd/unix-directory';
459
		});
460
	}
461
462
	/**
463
	 * Recursively remove all children of a folder
464
	 *
465
	 * @param array $entry the cache entry of the folder to remove the children of
466
	 * @throws \OC\DatabaseException
467
	 */
468 View Code Duplication
	private function removeChildren($entry) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
469
		$subFolders = $this->getSubFolders($entry);
470
		foreach ($subFolders as $folder) {
471
			$this->removeChildren($folder);
472
		}
473
		$sql = 'DELETE FROM `*PREFIX*filecache` WHERE `parent` = ?';
474
		$this->connection->executeQuery($sql, array($entry['fileid']));
475
	}
476
477
	/**
478
	 * Move a file or folder in the cache
479
	 *
480
	 * @param string $source
481
	 * @param string $target
482
	 */
483
	public function move($source, $target) {
484
		$this->moveFromCache($this, $source, $target);
485
	}
486
487
	/**
488
	 * Get the storage id and path needed for a move
489
	 *
490
	 * @param string $path
491
	 * @return array [$storageId, $internalPath]
492
	 */
493
	protected function getMoveInfo($path) {
494
		return [$this->getNumericStorageId(), $path];
495
	}
496
497
	/**
498
	 * Move a file or folder in the cache
499
	 *
500
	 * @param \OCP\Files\Cache\ICache $sourceCache
501
	 * @param string $sourcePath
502
	 * @param string $targetPath
503
	 * @throws \OC\DatabaseException
504
	 * @throws \Exception if the given storages have an invalid id
505
	 */
506
	public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
507
		if ($sourceCache instanceof Cache) {
508
			// normalize source and target
509
			$sourcePath = $this->normalize($sourcePath);
510
			$targetPath = $this->normalize($targetPath);
511
512
			$sourceData = $sourceCache->get($sourcePath);
513
			$sourceId = $sourceData['fileid'];
514
			$newParentId = $this->getParentId($targetPath);
515
516
			list($sourceStorageId, $sourcePath) = $sourceCache->getMoveInfo($sourcePath);
517
			list($targetStorageId, $targetPath) = $this->getMoveInfo($targetPath);
518
519
			if (is_null($sourceStorageId) || $sourceStorageId === false) {
520
				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
521
			}
522
			if (is_null($targetStorageId) || $targetStorageId === false) {
523
				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
524
			}
525
526
			$this->connection->beginTransaction();
527
			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
528
				//update all child entries
529
				$sourceLength = strlen($sourcePath);
530
				$query = $this->connection->getQueryBuilder();
531
532
				$fun = $query->func();
533
				$newPathFunction = $fun->concat(
534
					$query->createNamedParameter($targetPath),
535
					$fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
536
				);
537
				$query->update('filecache')
538
					->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
539
					->set('path_hash', $fun->md5($newPathFunction))
540
					->set('path', $newPathFunction)
541
					->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
542
					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
543
544
				try {
545
					$query->execute();
546
				} catch (\OC\DatabaseException $e) {
547
					$this->connection->rollBack();
548
					throw $e;
549
				}
550
			}
551
552
			$sql = 'UPDATE `*PREFIX*filecache` SET `storage` = ?, `path` = ?, `path_hash` = ?, `name` = ?, `parent` = ? WHERE `fileid` = ?';
553
			$this->connection->executeQuery($sql, array($targetStorageId, $targetPath, md5($targetPath), \OC_Util::basename($targetPath), $newParentId, $sourceId));
554
			$this->connection->commit();
555
		} else {
556
			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
557
		}
558
	}
559
560
	/**
561
	 * remove all entries for files that are stored on the storage from the cache
562
	 */
563
	public function clear() {
564
		$sql = 'DELETE FROM `*PREFIX*filecache` WHERE `storage` = ?';
565
		$this->connection->executeQuery($sql, array($this->getNumericStorageId()));
566
567
		$sql = 'DELETE FROM `*PREFIX*storages` WHERE `id` = ?';
568
		$this->connection->executeQuery($sql, array($this->storageId));
569
	}
570
571
	/**
572
	 * Get the scan status of a file
573
	 *
574
	 * - Cache::NOT_FOUND: File is not in the cache
575
	 * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
576
	 * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
577
	 * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
578
	 *
579
	 * @param string $file
580
	 *
581
	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
582
	 */
583
	public function getStatus($file) {
584
		// normalize file
585
		$file = $this->normalize($file);
586
587
		$pathHash = md5($file);
588
		$sql = 'SELECT `size` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path_hash` = ?';
589
		$result = $this->connection->executeQuery($sql, array($this->getNumericStorageId(), $pathHash));
590
		if ($row = $result->fetch()) {
591
			if ((int)$row['size'] === -1) {
592
				return self::SHALLOW;
593
			} else {
594
				return self::COMPLETE;
595
			}
596
		} else {
597
			if (isset($this->partial[$file])) {
598
				return self::PARTIAL;
599
			} else {
600
				return self::NOT_FOUND;
601
			}
602
		}
603
	}
604
605
	/**
606
	 * search for files matching $pattern
607
	 *
608
	 * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
609
	 * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
610
	 */
611
	public function search($pattern) {
612
		// normalize pattern
613
		$pattern = $this->normalize($pattern);
614
615
		if ($pattern === '%%') {
616
			return [];
617
		}
618
619
620
		$sql = '
621
			SELECT `fileid`, `storage`, `path`, `parent`, `name`,
622
				`mimetype`, `storage_mtime`, `mimepart`, `size`, `mtime`,
623
				 `encrypted`, `etag`, `permissions`, `checksum`
624
			FROM `*PREFIX*filecache`
625
			WHERE `storage` = ? AND `name` ILIKE ?';
626
		$result = $this->connection->executeQuery($sql,
627
			[$this->getNumericStorageId(), $pattern]
628
		);
629
630
		return $this->searchResultToCacheEntries($result);
631
	}
632
633
	/**
634
	 * @param Statement $result
635
	 * @return CacheEntry[]
636
	 */
637
	private function searchResultToCacheEntries(Statement $result) {
638
		$files = $result->fetchAll();
639
640
		return array_map(function (array $data) {
641
			return self::cacheEntryFromData($data, $this->mimetypeLoader);
642
		}, $files);
643
	}
644
645
	/**
646
	 * search for files by mimetype
647
	 *
648
	 * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
649
	 *        where it will search for all mimetypes in the group ('image/*')
650
	 * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
651
	 */
652
	public function searchByMime($mimetype) {
653
		if (strpos($mimetype, '/')) {
654
			$where = '`mimetype` = ?';
655
		} else {
656
			$where = '`mimepart` = ?';
657
		}
658
		$sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `storage_mtime`, `mtime`, `encrypted`, `etag`, `permissions`, `checksum`
659
				FROM `*PREFIX*filecache` WHERE ' . $where . ' AND `storage` = ?';
660
		$mimetype = $this->mimetypeLoader->getId($mimetype);
661
		$result = $this->connection->executeQuery($sql, array($mimetype, $this->getNumericStorageId()));
662
663
		return $this->searchResultToCacheEntries($result);
664
	}
665
666
	public function searchQuery(ISearchQuery $searchQuery) {
667
		$builder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
668
669
		$query = $builder->select(['fileid', 'storage', 'path', 'parent', 'name', 'mimetype', 'mimepart', 'size', 'mtime', 'storage_mtime', 'encrypted', 'etag', 'permissions', 'checksum'])
670
			->from('filecache', 'file');
671
672
		$query->where($builder->expr()->eq('storage', $builder->createNamedParameter($this->getNumericStorageId())));
673
674
		if ($this->querySearchHelper->shouldJoinTags($searchQuery->getSearchOperation())) {
675
			$query
676
				->innerJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid'))
677
				->innerJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX(
0 ignored issues
show
Documentation introduced by
$builder->expr()->andX($...categoryid', 'tag.id')) is of type object<OCP\DB\QueryBuilder\ICompositeExpression>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
678
					$builder->expr()->eq('tagmap.type', 'tag.type'),
679
					$builder->expr()->eq('tagmap.categoryid', 'tag.id')
680
				))
681
				->andWhere($builder->expr()->eq('tag.type', $builder->createNamedParameter('files')))
682
				->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($searchQuery->getUser()->getUID())));
683
		}
684
685
		$query->andWhere($this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation()));
686
687
		$this->querySearchHelper->addSearchOrdersToQuery($query, $searchQuery->getOrder());
688
689
		if ($searchQuery->getLimit()) {
690
			$query->setMaxResults($searchQuery->getLimit());
691
		}
692
		if ($searchQuery->getOffset()) {
693
			$query->setFirstResult($searchQuery->getOffset());
694
		}
695
696
		$result = $query->execute();
697
		return $this->searchResultToCacheEntries($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $query->execute() on line 696 can also be of type integer; however, OC\Files\Cache\Cache::searchResultToCacheEntries() does only seem to accept object<Doctrine\DBAL\Driver\Statement>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
698
	}
699
700
	/**
701
	 * Search for files by tag of a given users.
702
	 *
703
	 * Note that every user can tag files differently.
704
	 *
705
	 * @param string|int $tag name or tag id
706
	 * @param string $userId owner of the tags
707
	 * @return ICacheEntry[] file data
708
	 */
709
	public function searchByTag($tag, $userId) {
710
		$sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, ' .
711
			'`mimetype`, `mimepart`, `size`, `mtime`, `storage_mtime`, ' .
712
			'`encrypted`, `etag`, `permissions`, `checksum` ' .
713
			'FROM `*PREFIX*filecache` `file`, ' .
714
			'`*PREFIX*vcategory_to_object` `tagmap`, ' .
715
			'`*PREFIX*vcategory` `tag` ' .
716
			// JOIN filecache to vcategory_to_object
717
			'WHERE `file`.`fileid` = `tagmap`.`objid` ' .
718
			// JOIN vcategory_to_object to vcategory
719
			'AND `tagmap`.`type` = `tag`.`type` ' .
720
			'AND `tagmap`.`categoryid` = `tag`.`id` ' .
721
			// conditions
722
			'AND `file`.`storage` = ? ' .
723
			'AND `tag`.`type` = \'files\' ' .
724
			'AND `tag`.`uid` = ? ';
725
		if (is_int($tag)) {
726
			$sql .= 'AND `tag`.`id` = ? ';
727
		} else {
728
			$sql .= 'AND `tag`.`category` = ? ';
729
		}
730
		$result = $this->connection->executeQuery(
731
			$sql,
732
			[
733
				$this->getNumericStorageId(),
734
				$userId,
735
				$tag
736
			]
737
		);
738
739
		$files = $result->fetchAll();
740
741
		return array_map(function (array $data) {
742
			return self::cacheEntryFromData($data, $this->mimetypeLoader);
743
		}, $files);
744
	}
745
746
	/**
747
	 * Re-calculate the folder size and the size of all parent folders
748
	 *
749
	 * @param string|boolean $path
750
	 * @param array $data (optional) meta data of the folder
751
	 */
752
	public function correctFolderSize($path, $data = null) {
753
		$this->calculateFolderSize($path, $data);
0 ignored issues
show
Bug introduced by
It seems like $path defined by parameter $path on line 752 can also be of type boolean; however, OC\Files\Cache\Cache::calculateFolderSize() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
754
		if ($path !== '') {
755
			$parent = dirname($path);
756
			if ($parent === '.' or $parent === '/') {
757
				$parent = '';
758
			}
759
			$this->correctFolderSize($parent);
760
		}
761
	}
762
763
	/**
764
	 * calculate the size of a folder and set it in the cache
765
	 *
766
	 * @param string $path
767
	 * @param array $entry (optional) meta data of the folder
768
	 * @return int
769
	 */
770
	public function calculateFolderSize($path, $entry = null) {
771
		$totalSize = 0;
772
		if (is_null($entry) or !isset($entry['fileid'])) {
773
			$entry = $this->get($path);
774
		}
775
		if (isset($entry['mimetype']) && $entry['mimetype'] === 'httpd/unix-directory') {
776
			$id = $entry['fileid'];
777
			$sql = 'SELECT SUM(`size`) AS f1, MIN(`size`) AS f2 ' .
778
				'FROM `*PREFIX*filecache` ' .
779
				'WHERE `parent` = ? AND `storage` = ?';
780
			$result = $this->connection->executeQuery($sql, array($id, $this->getNumericStorageId()));
781
			if ($row = $result->fetch()) {
782
				$result->closeCursor();
783
				list($sum, $min) = array_values($row);
784
				$sum = 0 + $sum;
785
				$min = 0 + $min;
786
				if ($min === -1) {
787
					$totalSize = $min;
788
				} else {
789
					$totalSize = $sum;
790
				}
791
				$update = array();
792
				if ($entry['size'] !== $totalSize) {
793
					$update['size'] = $totalSize;
794
				}
795
				if (count($update) > 0) {
796
					$this->update($id, $update);
797
				}
798
			} else {
799
				$result->closeCursor();
800
			}
801
		}
802
		return $totalSize;
803
	}
804
805
	/**
806
	 * get all file ids on the files on the storage
807
	 *
808
	 * @return int[]
809
	 */
810
	public function getAll() {
811
		$sql = 'SELECT `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ?';
812
		$result = $this->connection->executeQuery($sql, array($this->getNumericStorageId()));
813
		$ids = array();
814
		while ($row = $result->fetch()) {
815
			$ids[] = $row['fileid'];
816
		}
817
		return $ids;
818
	}
819
820
	/**
821
	 * find a folder in the cache which has not been fully scanned
822
	 *
823
	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
824
	 * use the one with the highest id gives the best result with the background scanner, since that is most
825
	 * likely the folder where we stopped scanning previously
826
	 *
827
	 * @return string|bool the path of the folder or false when no folder matched
828
	 */
829
	public function getIncomplete() {
830
		$query = $this->connection->prepare('SELECT `path` FROM `*PREFIX*filecache`'
831
			. ' WHERE `storage` = ? AND `size` = -1 ORDER BY `fileid` DESC', 1);
832
		$query->execute([$this->getNumericStorageId()]);
833
		if ($row = $query->fetch()) {
834
			return $row['path'];
835
		} else {
836
			return false;
837
		}
838
	}
839
840
	/**
841
	 * get the path of a file on this storage by it's file id
842
	 *
843
	 * @param int $id the file id of the file or folder to search
844
	 * @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
845
	 */
846
	public function getPathById($id) {
847
		$sql = 'SELECT `path` FROM `*PREFIX*filecache` WHERE `fileid` = ? AND `storage` = ?';
848
		$result = $this->connection->executeQuery($sql, array($id, $this->getNumericStorageId()));
849
		if ($row = $result->fetch()) {
850
			// Oracle stores empty strings as null...
851
			if ($row['path'] === null) {
852
				return '';
853
			}
854
			return $row['path'];
855
		} else {
856
			return null;
857
		}
858
	}
859
860
	/**
861
	 * get the storage id of the storage for a file and the internal path of the file
862
	 * unlike getPathById this does not limit the search to files on this storage and
863
	 * instead does a global search in the cache table
864
	 *
865
	 * @param int $id
866
	 * @deprecated use getPathById() instead
867
	 * @return array first element holding the storage id, second the path
868
	 */
869
	static public function getById($id) {
870
		$connection = \OC::$server->getDatabaseConnection();
871
		$sql = 'SELECT `storage`, `path` FROM `*PREFIX*filecache` WHERE `fileid` = ?';
872
		$result = $connection->executeQuery($sql, array($id));
873
		if ($row = $result->fetch()) {
874
			$numericId = $row['storage'];
875
			$path = $row['path'];
876
		} else {
877
			return null;
878
		}
879
880
		if ($id = Storage::getStorageId($numericId)) {
881
			return array($id, $path);
882
		} else {
883
			return null;
884
		}
885
	}
886
887
	/**
888
	 * normalize the given path
889
	 *
890
	 * @param string $path
891
	 * @return string
892
	 */
893
	public function normalize($path) {
894
895
		return trim(\OC_Util::normalizeUnicode($path), '/');
896
	}
897
}
898