Passed
Push — master ( 70aa85...772303 )
by Morris
25:14 queued 14:09
created

Cache::searchResultToCacheEntries()   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 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\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 = array();
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();
112
		$this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
113
	}
114
115
	/**
116
	 * Get the numeric storage id for this cache's storage
117
	 *
118
	 * @return int
119
	 */
120
	public function getNumericStorageId() {
121
		return $this->storageCache->getNumericId();
122
	}
123
124
	/**
125
	 * get the stored metadata of a file or folder
126
	 *
127
	 * @param string | int $file either the path of a file or folder or the file id for a file or folder
128
	 * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
129
	 */
130
	public function get($file) {
131
		if (is_string($file) or $file == '') {
132
			// normalize file
133
			$file = $this->normalize($file);
134
135
			$where = 'WHERE `storage` = ? AND `path_hash` = ?';
136
			$params = array($this->getNumericStorageId(), md5($file));
137
		} else { //file id
138
			$where = 'WHERE `fileid` = ?';
139
			$params = array($file);
140
		}
141
		$sql = 'SELECT `fileid`, `storage`, `path`, `path_hash`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`,
142
					   `storage_mtime`, `encrypted`, `etag`, `permissions`, `checksum`
143
				FROM `*PREFIX*filecache` ' . $where;
144
		$result = $this->connection->executeQuery($sql, $params);
145
		$data = $result->fetch();
146
147
		//FIXME hide this HACK in the next database layer, or just use doctrine and get rid of MDB2 and PDO
148
		//PDO returns false, MDB2 returns null, oracle always uses MDB2, so convert null to false
149
		if ($data === null) {
150
			$data = false;
151
		}
152
153
		//merge partial data
154
		if (!$data and is_string($file)) {
155
			if (isset($this->partial[$file])) {
156
				$data = $this->partial[$file];
157
			}
158
			return $data;
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
		return new CacheEntry($data);
191
	}
192
193
	/**
194
	 * get the metadata of all files stored in $folder
195
	 *
196
	 * @param string $folder
197
	 * @return ICacheEntry[]
198
	 */
199
	public function getFolderContents($folder) {
200
		$fileId = $this->getId($folder);
201
		return $this->getFolderContentsById($fileId);
202
	}
203
204
	/**
205
	 * get the metadata of all files stored in $folder
206
	 *
207
	 * @param int $fileId the file id of the folder
208
	 * @return ICacheEntry[]
209
	 */
210
	public function getFolderContentsById($fileId) {
211
		if ($fileId > -1) {
212
			$sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`,
213
						   `storage_mtime`, `encrypted`, `etag`, `permissions`, `checksum`
214
					FROM `*PREFIX*filecache` WHERE `parent` = ? ORDER BY `name` ASC';
215
			$result = $this->connection->executeQuery($sql, [$fileId]);
216
			$files = $result->fetchAll();
217
			return array_map(function (array $data) {
218
				return self::cacheEntryFromData($data, $this->mimetypeLoader);
219
			}, $files);
220
		}
221
		return [];
222
	}
223
224
	/**
225
	 * insert or update meta data for a file or folder
226
	 *
227
	 * @param string $file
228
	 * @param array $data
229
	 *
230
	 * @return int file id
231
	 * @throws \RuntimeException
232
	 */
233
	public function put($file, array $data) {
234
		if (($id = $this->getId($file)) > -1) {
235
			$this->update($id, $data, $file);
0 ignored issues
show
Unused Code introduced by
The call to OC\Files\Cache\Cache::update() has too many arguments starting with $file. ( Ignorable by Annotation )

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

235
			$this->/** @scrutinizer ignore-call */ 
236
          update($id, $data, $file);

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

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

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

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

703
				->innerJoin('tagmap', 'vcategory', 'tag', /** @scrutinizer ignore-type */ $builder->expr()->andX(
Loading history...
704
					$builder->expr()->eq('tagmap.type', 'tag.type'),
705
					$builder->expr()->eq('tagmap.categoryid', 'tag.id')
706
				))
707
				->andWhere($builder->expr()->eq('tag.type', $builder->createNamedParameter('files')))
708
				->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($searchQuery->getUser()->getUID())));
709
		}
710
711
		$query->andWhere($this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation()));
712
713
		$this->querySearchHelper->addSearchOrdersToQuery($query, $searchQuery->getOrder());
714
715
		if ($searchQuery->getLimit()) {
716
			$query->setMaxResults($searchQuery->getLimit());
717
		}
718
		if ($searchQuery->getOffset()) {
719
			$query->setFirstResult($searchQuery->getOffset());
720
		}
721
722
		$result = $query->execute();
723
		return $this->searchResultToCacheEntries($result);
724
	}
725
726
	/**
727
	 * Search for files by tag of a given users.
728
	 *
729
	 * Note that every user can tag files differently.
730
	 *
731
	 * @param string|int $tag name or tag id
732
	 * @param string $userId owner of the tags
733
	 * @return ICacheEntry[] file data
734
	 */
735
	public function searchByTag($tag, $userId) {
736
		$sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, ' .
737
			'`mimetype`, `mimepart`, `size`, `mtime`, `storage_mtime`, ' .
738
			'`encrypted`, `etag`, `permissions`, `checksum` ' .
739
			'FROM `*PREFIX*filecache` `file`, ' .
740
			'`*PREFIX*vcategory_to_object` `tagmap`, ' .
741
			'`*PREFIX*vcategory` `tag` ' .
742
			// JOIN filecache to vcategory_to_object
743
			'WHERE `file`.`fileid` = `tagmap`.`objid` ' .
744
			// JOIN vcategory_to_object to vcategory
745
			'AND `tagmap`.`type` = `tag`.`type` ' .
746
			'AND `tagmap`.`categoryid` = `tag`.`id` ' .
747
			// conditions
748
			'AND `file`.`storage` = ? ' .
749
			'AND `tag`.`type` = \'files\' ' .
750
			'AND `tag`.`uid` = ? ';
751
		if (is_int($tag)) {
752
			$sql .= 'AND `tag`.`id` = ? ';
753
		} else {
754
			$sql .= 'AND `tag`.`category` = ? ';
755
		}
756
		$result = $this->connection->executeQuery(
757
			$sql,
758
			[
759
				$this->getNumericStorageId(),
760
				$userId,
761
				$tag
762
			]
763
		);
764
765
		$files = $result->fetchAll();
766
767
		return array_map(function (array $data) {
768
			return self::cacheEntryFromData($data, $this->mimetypeLoader);
769
		}, $files);
770
	}
771
772
	/**
773
	 * Re-calculate the folder size and the size of all parent folders
774
	 *
775
	 * @param string|boolean $path
776
	 * @param array $data (optional) meta data of the folder
777
	 */
778
	public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
779
		$this->calculateFolderSize($path, $data);
780
		if ($path !== '') {
781
			$parent = dirname($path);
782
			if ($parent === '.' or $parent === '/') {
783
				$parent = '';
784
			}
785
			if ($isBackgroundScan) {
786
				$parentData = $this->get($parent);
787
				if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
788
					$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

788
					$this->correctFolderSize($parent, /** @scrutinizer ignore-type */ $parentData, $isBackgroundScan);
Loading history...
789
				}
790
			} else {
791
				$this->correctFolderSize($parent);
792
			}
793
		}
794
	}
795
796
	/**
797
	 * get the incomplete count that shares parent $folder
798
	 *
799
	 * @param int $fileId the file id of the folder
800
	 * @return int
801
	 */
802
	public function getIncompleteChildrenCount($fileId) {
803
		if ($fileId > -1) {
804
			$sql = 'SELECT count(*)
805
					FROM `*PREFIX*filecache` WHERE `parent` = ? AND size = -1';
806
			$result = $this->connection->executeQuery($sql, [$fileId]);
807
			return (int)$result->fetchColumn();
808
		}
809
		return -1;
810
	}
811
812
	/**
813
	 * calculate the size of a folder and set it in the cache
814
	 *
815
	 * @param string $path
816
	 * @param array $entry (optional) meta data of the folder
817
	 * @return int
818
	 */
819
	public function calculateFolderSize($path, $entry = null) {
820
		$totalSize = 0;
821
		if (is_null($entry) or !isset($entry['fileid'])) {
822
			$entry = $this->get($path);
823
		}
824
		if (isset($entry['mimetype']) && $entry['mimetype'] === 'httpd/unix-directory') {
825
			$id = $entry['fileid'];
826
			$sql = 'SELECT SUM(`size`) AS f1, MIN(`size`) AS f2 ' .
827
				'FROM `*PREFIX*filecache` ' .
828
				'WHERE `parent` = ? AND `storage` = ?';
829
			$result = $this->connection->executeQuery($sql, array($id, $this->getNumericStorageId()));
830
			if ($row = $result->fetch()) {
831
				$result->closeCursor();
832
				list($sum, $min) = array_values($row);
833
				$sum = 0 + $sum;
834
				$min = 0 + $min;
835
				if ($min === -1) {
836
					$totalSize = $min;
837
				} else {
838
					$totalSize = $sum;
839
				}
840
				$update = array();
841
				if ($entry['size'] !== $totalSize) {
842
					$update['size'] = $totalSize;
843
				}
844
				if (count($update) > 0) {
845
					$this->update($id, $update);
846
				}
847
			} else {
848
				$result->closeCursor();
849
			}
850
		}
851
		return $totalSize;
852
	}
853
854
	/**
855
	 * get all file ids on the files on the storage
856
	 *
857
	 * @return int[]
858
	 */
859
	public function getAll() {
860
		$sql = 'SELECT `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ?';
861
		$result = $this->connection->executeQuery($sql, array($this->getNumericStorageId()));
862
		$ids = array();
863
		while ($row = $result->fetch()) {
864
			$ids[] = $row['fileid'];
865
		}
866
		return $ids;
867
	}
868
869
	/**
870
	 * find a folder in the cache which has not been fully scanned
871
	 *
872
	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
873
	 * use the one with the highest id gives the best result with the background scanner, since that is most
874
	 * likely the folder where we stopped scanning previously
875
	 *
876
	 * @return string|bool the path of the folder or false when no folder matched
877
	 */
878
	public function getIncomplete() {
879
		$query = $this->connection->prepare('SELECT `path` FROM `*PREFIX*filecache`'
880
			. ' WHERE `storage` = ? AND `size` = -1 ORDER BY `fileid` DESC', 1);
881
		$query->execute([$this->getNumericStorageId()]);
882
		if ($row = $query->fetch()) {
883
			return $row['path'];
884
		} else {
885
			return false;
886
		}
887
	}
888
889
	/**
890
	 * get the path of a file on this storage by it's file id
891
	 *
892
	 * @param int $id the file id of the file or folder to search
893
	 * @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
894
	 */
895
	public function getPathById($id) {
896
		$sql = 'SELECT `path` FROM `*PREFIX*filecache` WHERE `fileid` = ? AND `storage` = ?';
897
		$result = $this->connection->executeQuery($sql, array($id, $this->getNumericStorageId()));
898
		if ($row = $result->fetch()) {
899
			// Oracle stores empty strings as null...
900
			if ($row['path'] === null) {
901
				return '';
902
			}
903
			return $row['path'];
904
		} else {
905
			return null;
906
		}
907
	}
908
909
	/**
910
	 * get the storage id of the storage for a file and the internal path of the file
911
	 * unlike getPathById this does not limit the search to files on this storage and
912
	 * instead does a global search in the cache table
913
	 *
914
	 * @param int $id
915
	 * @deprecated use getPathById() instead
916
	 * @return array first element holding the storage id, second the path
917
	 */
918
	static public function getById($id) {
919
		$connection = \OC::$server->getDatabaseConnection();
920
		$sql = 'SELECT `storage`, `path` FROM `*PREFIX*filecache` WHERE `fileid` = ?';
921
		$result = $connection->executeQuery($sql, array($id));
922
		if ($row = $result->fetch()) {
923
			$numericId = $row['storage'];
924
			$path = $row['path'];
925
		} else {
926
			return null;
927
		}
928
929
		if ($id = Storage::getStorageId($numericId)) {
930
			return array($id, $path);
931
		} else {
932
			return null;
933
		}
934
	}
935
936
	/**
937
	 * normalize the given path
938
	 *
939
	 * @param string $path
940
	 * @return string
941
	 */
942
	public function normalize($path) {
943
944
		return trim(\OC_Util::normalizeUnicode($path), '/');
945
	}
946
}