1<?php
2/**
3 * @author Andreas Fischer <bantu@owncloud.com>
4 * @author Björn Schießle <bjoern@schiessle.org>
5 * @author Florin Peter <github@florin-peter.de>
6 * @author Jens-Christian Fischer <jens-christian.fischer@switch.ch>
7 * @author Joas Schilling <coding@schilljs.com>
8 * @author Jörn Friedrich Dreyer <jfd@butonic.de>
9 * @author Lukas Reschke <lukas@statuscode.ch>
10 * @author Michael Gapczynski <GapczynskiM@gmail.com>
11 * @author Morris Jobke <hey@morrisjobke.de>
12 * @author Robin Appelman <icewind@owncloud.com>
13 * @author Robin McCorkell <robin@mccorkell.me.uk>
14 * @author Roeland Jago Douma <rullzer@owncloud.com>
15 * @author TheSFReader <TheSFReader@gmail.com>
16 * @author Thomas Müller <thomas.mueller@tmit.eu>
17 * @author Vincent Petry <pvince81@owncloud.com>
18 *
19 * @copyright Copyright (c) 2018, ownCloud GmbH
20 * @license AGPL-3.0
21 *
22 * This code is free software: you can redistribute it and/or modify
23 * it under the terms of the GNU Affero General Public License, version 3,
24 * as published by the Free Software Foundation.
25 *
26 * This program is distributed in the hope that it will be useful,
27 * but WITHOUT ANY WARRANTY; without even the implied warranty of
28 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 * GNU Affero General Public License for more details.
30 *
31 * You should have received a copy of the GNU Affero General Public License, version 3,
32 * along with this program.  If not, see <http://www.gnu.org/licenses/>
33 *
34 */
35
36namespace OC\Files\Cache;
37
38use Doctrine\DBAL\Platforms\OraclePlatform;
39use OCP\DB\QueryBuilder\IQueryBuilder;
40use OCP\Files\Cache\ICache;
41use OCP\Files\Cache\IScanner;
42use OCP\Files\Cache\ICacheEntry;
43use \OCP\Files\IMimeTypeLoader;
44use OCP\IDBConnection;
45
46/**
47 * Metadata cache for a storage
48 *
49 * The cache stores the metadata for all files and folders in a storage and is kept up to date trough the following mechanisms:
50 *
51 * - Scanner: scans the storage and updates the cache where needed
52 * - Watcher: checks for changes made to the filesystem outside of the ownCloud instance and rescans files and folder when a change is detected
53 * - Updater: listens to changes made to the filesystem inside of the ownCloud instance and updates the cache where needed
54 * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
55 */
56class Cache implements ICache {
57	use MoveFromCacheTrait {
58		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
59	}
60
61	/**
62	 * @var array partial data for the cache
63	 */
64	protected $partial = [];
65
66	/**
67	 * @var string
68	 */
69	protected $storageId;
70
71	/**
72	 * @var Storage $storageCache
73	 */
74	protected $storageCache;
75
76	/** @var IMimeTypeLoader */
77	protected $mimetypeLoader;
78
79	/**
80	 * @var IDBConnection
81	 */
82	protected $connection;
83
84	/**
85	 * @param \OC\Files\Storage\Storage|string $storage
86	 */
87	public function __construct($storage) {
88		if ($storage instanceof \OC\Files\Storage\Storage) {
89			$this->storageId = $storage->getId();
90		} else {
91			$this->storageId = $storage;
92		}
93		if (\strlen($this->storageId) > 64) {
94			$this->storageId = \md5($this->storageId);
95		}
96
97		$this->storageCache = new Storage($storage);
98		$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
99		$this->connection = \OC::$server->getDatabaseConnection();
100	}
101
102	/**
103	 * Get the numeric storage id for this cache's storage
104	 *
105	 * @return int
106	 */
107	public function getNumericStorageId() {
108		return $this->storageCache->getNumericId();
109	}
110
111	/**
112	 * get the stored metadata of a file or folder
113	 *
114	 * @param string | int $file either the path of a file or folder or the file id for a file or folder
115	 * @return ICacheEntry|false the cache entry as array or false if the file is not found in the cache
116	 */
117	public function get($file) {
118		if (\is_string($file) or $file == '') {
119			// normalize file
120			$file = $this->normalize($file);
121
122			$where = 'WHERE `storage` = ? AND `path_hash` = ?';
123			$params = [$this->getNumericStorageId(), \md5($file)];
124		} else { //file id
125			$where = 'WHERE `fileid` = ?';
126			$params = [$file];
127		}
128		$sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`,
129					   `storage_mtime`, `encrypted`, `etag`, `permissions`, `checksum`
130				FROM `*PREFIX*filecache` ' . $where;
131		$result = $this->connection->executeQuery($sql, $params);
132		$data = $result->fetch();
133
134		//FIXME hide this HACK in the next database layer, or just use doctrine and get rid of MDB2 and PDO
135		//PDO returns false, MDB2 returns null, oracle always uses MDB2, so convert null to false
136		if ($data === null) {
137			$data = false;
138		}
139
140		//merge partial data
141		if ($data) {
142			//fix types
143			$data['fileid'] = (int)$data['fileid'];
144			$data['parent'] = (int)$data['parent'];
145			$data['size'] = 0 + $data['size'];
146			$data['mtime'] = (int)$data['mtime'];
147			$data['storage_mtime'] = (int)$data['storage_mtime'];
148			$data['encryptedVersion'] = (int)$data['encrypted'];
149			$data['encrypted'] = (bool)$data['encrypted'];
150			$data['storage'] = $this->storageId;
151			$data['mimetype'] = $this->mimetypeLoader->getMimetypeById($data['mimetype']);
152			$data['mimepart'] = $this->mimetypeLoader->getMimetypeById($data['mimepart']);
153			if ($data['storage_mtime'] == 0) {
154				$data['storage_mtime'] = $data['mtime'];
155			}
156			$data['permissions'] = (int)$data['permissions'];
157			// Oracle stores empty strings as null...
158			if ($data['name'] === null) {
159				$data['name'] = '';
160			}
161			if ($data['path'] === null) {
162				$data['path'] = '';
163			}
164			return new CacheEntry($data);
165		} elseif (!$data and \is_string($file)) {
166			if (isset($this->partial[$file])) {
167				$data = $this->partial[$file];
168			}
169			return $data;
170		} else {
171			return false;
172		}
173	}
174
175	/**
176	 * get the metadata of all files stored in $folder
177	 *
178	 * @param string $folder
179	 * @return ICacheEntry[]
180	 */
181	public function getFolderContents($folder) {
182		$fileId = $this->getId($folder);
183		return $this->getFolderContentsById($fileId);
184	}
185
186	/**
187	 * get the metadata of all files stored in $folder
188	 *
189	 * @param int $fileId the file id of the folder
190	 * @return ICacheEntry[]
191	 */
192	public function getFolderContentsById($fileId) {
193		return $this->getChildrenWithFilter($fileId, null);
194	}
195
196	private function getChildrenWithFilter($fileId, $mimetypeFilter = null) {
197		if ($fileId > -1) {
198			$qb = $this->connection->getQueryBuilder();
199			$qb->select(
200				'fileid',
201				'storage',
202				'path',
203				'parent',
204				'name',
205				'mimetype',
206				'mimepart',
207				'size',
208				'mtime',
209				'storage_mtime',
210				'encrypted',
211				'etag',
212				'permissions',
213				'checksum'
214			)
215				->from('filecache')
216				->where(
217					$qb->expr()->eq('parent', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))
218				);
219
220			if ($mimetypeFilter) {
221				$mimetypeId = $this->mimetypeLoader->getId($mimetypeFilter);
222				$qb->andWhere(
223					$qb->expr()->eq('mimetype', $qb->createNamedParameter($mimetypeId, IQueryBuilder::PARAM_INT))
224				);
225			}
226
227			$qb->orderBy('name', 'ASC');
228
229			$result = $qb->execute();
230			$files = $result->fetchAll();
231			foreach ($files as &$file) {
232				$file['mimetype'] = $this->mimetypeLoader->getMimetypeById($file['mimetype']);
233				$file['mimepart'] = $this->mimetypeLoader->getMimetypeById($file['mimepart']);
234				if ($file['storage_mtime'] == 0) {
235					$file['storage_mtime'] = $file['mtime'];
236				}
237				$file['permissions'] = (int)$file['permissions'];
238				$file['mtime'] = (int)$file['mtime'];
239				$file['storage_mtime'] = (int)$file['storage_mtime'];
240				$file['size'] = 0 + $file['size'];
241			}
242			return \array_map(function (array $data) {
243				return new CacheEntry($data);
244			}, $files);
245		} else {
246			return [];
247		}
248	}
249
250	/**
251	 * insert or update meta data for a file or folder
252	 *
253	 * @param string $file
254	 * @param array $data
255	 *
256	 * @return int file id
257	 * @throws \RuntimeException
258	 */
259	public function put($file, array $data) {
260		if (($id = $this->getId($file)) > -1) {
261			$this->update($id, $data);
262			return $id;
263		} else {
264			return $this->insert($file, $data);
265		}
266	}
267
268	/**
269	 * insert meta data for a new file or folder
270	 *
271	 * @param string $file
272	 * @param array $data
273	 *
274	 * @return int file id
275	 * @throws \RuntimeException
276	 */
277	public function insert($file, array $data) {
278		// normalize file
279		$file = $this->normalize($file);
280
281		if (isset($this->partial[$file])) { //add any saved partial data
282			$data = \array_merge($this->partial[$file], $data);
283			unset($this->partial[$file]);
284		}
285
286		$requiredFields = ['size', 'mtime', 'mimetype'];
287		foreach ($requiredFields as $field) {
288			if (!isset($data[$field])) { //data not complete save as partial and return
289				$this->partial[$file] = $data;
290				return -1;
291			}
292		}
293
294		$data['path'] = $file;
295		$data['parent'] = $this->getParentId($file);
296		$data['name'] = \OC_Util::basename($file);
297
298		list($queryParts, $params) = $this->buildParts($data);
299		$queryParts[] = '`storage`';
300		$params[] = $this->getNumericStorageId();
301
302		$queryParts = \array_map(function ($item) {
303			return \trim($item, "`");
304		}, $queryParts);
305		$values = \array_combine($queryParts, $params);
306		// Update or insert this to the filecache
307		\OC::$server->getDatabaseConnection()->upsert(
308			'*PREFIX*filecache',
309			$values,
310			[
311				'storage',
312				'path_hash',
313			]
314		);
315		// Now return the id for this row - crappy that we have to select here
316		// GetID should already return a value if upsert returned a positive value
317		return (int)$this->getId($file);
318	}
319
320	/**
321	 * update the metadata of an existing file or folder in the cache
322	 *
323	 * @param int $id the fileid of the existing file or folder
324	 * @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
325	 */
326	public function update($id, array $data) {
327		if (isset($data['path'])) {
328			// normalize path
329			$data['path'] = $this->normalize($data['path']);
330		}
331
332		if (isset($data['name'])) {
333			// normalize path
334			$data['name'] = $this->normalize($data['name']);
335		}
336
337		// Oracle does not support empty string values so we convert them to nulls
338		// https://github.com/owncloud/core/issues/31692
339		if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
340			foreach ($data as $param => $value) {
341				if ($value === '') {
342					$data[$param] = null;
343				}
344			}
345		}
346
347		list($queryParts, $params) = $this->buildParts($data);
348
349		// don't update if the data we try to set is the same as the one in the record
350		// some databases (Postgres) don't like superfluous updates
351		if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
352			$whereParts = [];
353			$setParts = [];
354			for ($i = 0; $i < \count($queryParts); $i++) {
355				if ($params[$i] === null) {
356					$setParts[] = "{$queryParts[$i]} = NULL";
357					$whereParts[] = "{$queryParts[$i]} IS NOT NULL";
358				} else {
359					$setParts[] = "{$queryParts[$i]} = ?";
360					$whereParts[] = "({$queryParts[$i]} <> ? OR {$queryParts[$i]} IS NULL)";
361				}
362			}
363			$setClause = \implode(', ', $setParts);
364			$whereClause = \implode(' OR ', $whereParts);
365
366			// remove null values from the $params
367			$params = \array_values(\array_filter($params, function ($v) {
368				return $v !== null;
369			}));
370			// duplicate $params because we need the parts twice in the SQL statement
371			// once for the SET part, once in the WHERE clause
372			$params = \array_merge($params, $params);
373			$params[] = $id;
374
375			$sql = "UPDATE `*PREFIX*filecache` SET $setClause WHERE ($whereClause) AND `fileid` = ?";
376		} else {
377			// duplicate $params because we need the parts twice in the SQL statement
378			// once for the SET part, once in the WHERE clause
379			$params = \array_merge($params, $params);
380			$params[] = $id;
381			$sql = 'UPDATE `*PREFIX*filecache` SET ' . \implode(' = ?, ', $queryParts) . '=? ' .
382				'WHERE (' .
383				\implode(' <> ? OR ', $queryParts) . ' <> ? OR ' .
384				\implode(' IS NULL OR ', $queryParts) . ' IS NULL' .
385				') AND `fileid` = ? ';
386		}
387
388		$this->connection->executeQuery($sql, $params);
389	}
390
391	/**
392	 * extract query parts and params array from data array
393	 *
394	 * @param array $data
395	 * @return array [$queryParts, $params]
396	 *        $queryParts: string[], the (escaped) column names to be set in the query
397	 *        $params: mixed[], the new values for the columns, to be passed as params to the query
398	 */
399	protected function buildParts(array $data) {
400		$fields = [
401			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
402			'etag', 'permissions', 'checksum'];
403
404		$doNotCopyStorageMTime = false;
405		if (\array_key_exists('mtime', $data) && $data['mtime'] === null) {
406			// this horrific magic tells it to not copy storage_mtime to mtime
407			unset($data['mtime']);
408			$doNotCopyStorageMTime = true;
409		}
410
411		$params = [];
412		$queryParts = [];
413		foreach ($data as $name => $value) {
414			if (\array_search($name, $fields) !== false) {
415				if ($name === 'path') {
416					$params[] = \md5($value);
417					$queryParts[] = '`path_hash`';
418				} elseif ($name === 'mimetype') {
419					$params[] = $this->mimetypeLoader->getId(\substr($value, 0, \strpos($value, '/')));
420					$queryParts[] = '`mimepart`';
421					$value = $this->mimetypeLoader->getId($value);
422				} elseif ($name === 'storage_mtime') {
423					if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
424						$params[] = $value;
425						$queryParts[] = '`mtime`';
426					}
427				} elseif ($name === 'encrypted') {
428					if (isset($data['encryptedVersion'])) {
429						$value = $data['encryptedVersion'];
430					} else {
431						// Boolean to integer conversion
432						$value = $value ? 1 : 0;
433					}
434				}
435				$params[] = $value;
436				$queryParts[] = '`' . $name . '`';
437			}
438		}
439		return [$queryParts, $params];
440	}
441
442	/**
443	 * get the file id for a file
444	 *
445	 * 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
446	 *
447	 * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
448	 *
449	 * @param string $file
450	 * @return int
451	 */
452	public function getId($file) {
453		// normalize file
454		$file = $this->normalize($file);
455
456		$pathHash = \md5($file);
457
458		$sql = 'SELECT `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path_hash` = ?';
459		$result = $this->connection->executeQuery($sql, [$this->getNumericStorageId(), $pathHash]);
460		if ($row = $result->fetch()) {
461			return $row['fileid'];
462		} else {
463			return -1;
464		}
465	}
466
467	/**
468	 * get the id of the parent folder of a file
469	 *
470	 * @param string $file
471	 * @return int
472	 */
473	public function getParentId($file) {
474		if ($file === '') {
475			return -1;
476		} else {
477			$parent = $this->getParentPath($file);
478			return (int)$this->getId($parent);
479		}
480	}
481
482	private function getParentPath($path) {
483		$parent = \dirname($path);
484		if ($parent === '.') {
485			$parent = '';
486		}
487		return $parent;
488	}
489
490	/**
491	 * check if a file is available in the cache
492	 *
493	 * @param string $file
494	 * @return bool
495	 */
496	public function inCache($file) {
497		return $this->getId($file) != -1;
498	}
499
500	/**
501	 * remove a file or folder from the cache
502	 *
503	 * when removing a folder from the cache all files and folders inside the folder will be removed as well
504	 *
505	 * @param string $file
506	 */
507	public function remove($file) {
508		$entry = $this->get($file);
509		if ($entry !== false) {
510			$sql = 'DELETE FROM `*PREFIX*filecache` WHERE `fileid` = ?';
511			$this->connection->executeQuery($sql, [$entry['fileid']]);
512			if ($entry['mimetype'] === 'httpd/unix-directory') {
513				$this->removeChildren($entry);
514			}
515		}
516	}
517
518	/**
519	 * Get all sub folders of a folder
520	 *
521	 * @param array $entry the cache entry of the folder to get the subfolders for
522	 * @return array[] the cache entries for the subfolders
523	 */
524	private function getSubFolders($entry) {
525		return $this->getChildrenWithFilter($entry['fileid'], 'httpd/unix-directory');
526	}
527
528	/**
529	 * Recursively remove all children of a folder
530	 *
531	 * @param array $entry the cache entry of the folder to remove the children of
532	 * @throws \OC\DatabaseException
533	 */
534	private function removeChildren($entry) {
535		$subFolders = $this->getSubFolders($entry);
536		foreach ($subFolders as $folder) {
537			$this->removeChildren($folder);
538		}
539		$sql = 'DELETE FROM `*PREFIX*filecache` WHERE `parent` = ?';
540		$this->connection->executeQuery($sql, [$entry['fileid']]);
541	}
542
543	/**
544	 * Move a file or folder in the cache
545	 *
546	 * @param string $source
547	 * @param string $target
548	 */
549	public function move($source, $target) {
550		$this->moveFromCache($this, $source, $target);
551	}
552
553	/**
554	 * Get the storage id and path needed for a move
555	 *
556	 * @param string $path
557	 * @return array [$storageId, $internalPath]
558	 */
559	protected function getMoveInfo($path) {
560		return [$this->getNumericStorageId(), $path];
561	}
562
563	/**
564	 * Move a file or folder in the cache
565	 *
566	 * @param \OCP\Files\Cache\ICache $sourceCache
567	 * @param string $sourcePath
568	 * @param string $targetPath
569	 * @throws \OC\DatabaseException
570	 * @throws \Exception if the given storages have an invalid id
571	 */
572	public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
573		if ($sourceCache instanceof Cache) {
574			// normalize source and target
575			$sourcePath = $this->normalize($sourcePath);
576			$targetPath = $this->normalize($targetPath);
577
578			$sourceData = $sourceCache->get($sourcePath);
579			$sourceId = $sourceData['fileid'];
580			$newParentId = $this->getParentId($targetPath);
581
582			list($sourceStorageId, $sourcePath) = $sourceCache->getMoveInfo($sourcePath);
583			list($targetStorageId, $targetPath) = $this->getMoveInfo($targetPath);
584
585			if ($sourceStorageId === null || $sourceStorageId === false) {
586				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
587			}
588			if ($targetStorageId === null || $targetStorageId === false) {
589				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
590			}
591
592			// sql for final update
593			$moveSql = 'UPDATE `*PREFIX*filecache` SET `storage` =  ?, `path` = ?, `path_hash` = ?, `name` = ?, `parent` =? WHERE `fileid` = ?';
594
595			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
596				//find all child entries
597				$this->connection->beginTransaction();
598				$this->handleChildrenMove($sourceStorageId, $sourcePath, $targetStorageId, $targetPath);
599				$this->connection->executeQuery($moveSql, [$targetStorageId, $targetPath, \md5($targetPath), \basename($targetPath), $newParentId, $sourceId]);
600				$this->connection->commit();
601			} else {
602				$this->connection->executeQuery($moveSql, [$targetStorageId, $targetPath, \md5($targetPath), \basename($targetPath), $newParentId, $sourceId]);
603			}
604		} else {
605			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
606		}
607	}
608
609	private function handleChildrenMove($sourceStorageId, $sourcePath, $targetStorageId, $targetPath) {
610		$platformName = $this->connection->getDatabasePlatform()->getName();
611		$versionString = $this->connection->getDatabaseVersionString();
612		$versionArray = \explode('.', $versionString);
613
614		$sql = null;
615		$sqlParams = [
616			'sourceStorageId' => $sourceStorageId,
617			'sourcePath' => "$sourcePath/",
618			'targetStorageId' => $targetStorageId,
619			'targetPath' => "$targetPath/",
620			'sourcePathLike' => $this->connection->escapeLikeParameter("$sourcePath/") . '%'
621		];
622		switch ($platformName) {
623			case 'oracle':
624				if (\intval($versionArray[0]) < 12) {
625					$sql = 'UPDATE `*PREFIX*filecache`
626						SET `storage` = :targetStorageId,
627							`path_hash` = LOWER(dbms_obfuscation_toolkit.md5(input => UTL_RAW.cast_to_raw(REPLACE(`path`, :sourcePath, :targetPath)))),
628							`path` = REPLACE(`path`, :sourcePath, :targetPath)
629						WHERE `storage` = :sourceStorageId
630						AND `path` LIKE :sourcePathLike';
631				} else {
632					$sql = 'UPDATE `*PREFIX*filecache`
633						SET `storage` = :targetStorageId,
634							`path_hash` = LOWER(standard_hash(REPLACE(`path`, :sourcePath, :targetPath), \'MD5\')),
635							`path` = REPLACE(`path`, :sourcePath, :targetPath)
636						WHERE `storage` = :sourceStorageId
637						AND `path` LIKE :sourcePathLike';
638				}
639				break;
640			case 'mysql':
641			case 'postgresql':
642				$sql = 'UPDATE `*PREFIX*filecache`
643					SET `storage` = :targetStorageId,
644						`path_hash` = MD5(REPLACE(`path`, :sourcePath, :targetPath)),
645						`path` = REPLACE(`path`, :sourcePath, :targetPath)
646					WHERE `storage` = :sourceStorageId
647					AND `path` LIKE :sourcePathLike';
648				break;
649		}
650
651		// MariaDB should be included as mysql
652		// if there is an (optimized) sql query with the parameters, run it
653		if (isset($sql, $sqlParams)) {
654			$this->connection->executeQuery($sql, $sqlParams);
655			return;
656		}
657
658		// for other DBs (sqlite), we keep the old behaviour -> get the list and update one by one
659		$sql = 'SELECT `path`, `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path` LIKE ?';
660		$result = $this->connection->executeQuery($sql, [$sourceStorageId, $this->connection->escapeLikeParameter($sourcePath) . '/%']);
661		$childEntries = $result->fetchAll();
662		$sourceLength = \strlen($sourcePath);
663		$query = $this->connection->prepare('UPDATE `*PREFIX*filecache` SET `storage` = ?, `path` = ?, `path_hash` = ? WHERE `fileid` = ?');
664
665		foreach ($childEntries as $child) {
666			$newTargetPath = $targetPath . \substr($child['path'], $sourceLength);
667			$query->execute([$targetStorageId, $newTargetPath, \md5($newTargetPath), $child['fileid']]);
668		}
669	}
670
671	/**
672	 * remove all entries for files that are stored on the storage from the cache
673	 */
674	public function clear() {
675		Storage::remove($this->storageId);
676	}
677
678	/**
679	 * Get the scan status of a file
680	 *
681	 * - Cache::NOT_FOUND: File is not in the cache
682	 * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
683	 * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
684	 * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
685	 * - Cache::NOT_SCANNED: Only the folder is in the cache. The contents are unknown, so the folder needs a scan
686	 *
687	 * @param string $file
688	 *
689	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW, Cache::COMPLETE or Cache::NOT_SCANNED
690	 */
691	public function getStatus($file) {
692		// normalize file
693		$file = $this->normalize($file);
694
695		$pathHash = \md5($file);
696		$sql = 'SELECT `size` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path_hash` = ?';
697		$result = $this->connection->executeQuery($sql, [$this->getNumericStorageId(), $pathHash]);
698		if ($row = $result->fetch()) {
699			$size = (int)$row['size'];
700			if ($size === IScanner::SIZE_NEEDS_SCAN) {
701				return self::NOT_SCANNED;
702			} elseif ($size === IScanner::SIZE_SHALLOW_SCANNED) {
703				return self::SHALLOW;
704			} else {
705				return self::COMPLETE;
706			}
707		} else {
708			if (isset($this->partial[$file])) {
709				return self::PARTIAL;
710			} else {
711				return self::NOT_FOUND;
712			}
713		}
714	}
715
716	/**
717	 * search for files matching $pattern
718	 *
719	 * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
720	 * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
721	 */
722	public function search($pattern) {
723		// normalize pattern
724		$pattern = $this->normalize($pattern);
725
726		$sql = '
727			SELECT `fileid`, `storage`, `path`, `parent`, `name`,
728				`mimetype`, `mimepart`, `size`, `mtime`, `encrypted`,
729				`etag`, `permissions`, `checksum`
730			FROM `*PREFIX*filecache`
731			WHERE `storage` = ? AND `name` ILIKE ?';
732		$result = $this->connection->executeQuery(
733			$sql,
734			[$this->getNumericStorageId(), $pattern]
735		);
736
737		$files = [];
738		while ($row = $result->fetch()) {
739			$row['mimetype'] = $this->mimetypeLoader->getMimetypeById($row['mimetype']);
740			$row['mimepart'] = $this->mimetypeLoader->getMimetypeById($row['mimepart']);
741			$files[] = $row;
742		}
743		return \array_map(function (array $data) {
744			return new CacheEntry($data);
745		}, $files);
746	}
747
748	/**
749	 * search for files by mimetype
750	 *
751	 * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
752	 *        where it will search for all mimetypes in the group ('image/*')
753	 * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
754	 */
755	public function searchByMime($mimetype) {
756		if (\strpos($mimetype, '/')) {
757			$where = '`mimetype` = ?';
758		} else {
759			$where = '`mimepart` = ?';
760		}
761		$sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`, `encrypted`, `etag`, `permissions`, `checksum`
762				FROM `*PREFIX*filecache` WHERE ' . $where . ' AND `storage` = ?';
763		$mimetype = $this->mimetypeLoader->getId($mimetype);
764		$result = $this->connection->executeQuery($sql, [$mimetype, $this->getNumericStorageId()]);
765		$files = [];
766		while ($row = $result->fetch()) {
767			$row['mimetype'] = $this->mimetypeLoader->getMimetypeById($row['mimetype']);
768			$row['mimepart'] = $this->mimetypeLoader->getMimetypeById($row['mimepart']);
769			$files[] = $row;
770		}
771		return \array_map(function (array $data) {
772			return new CacheEntry($data);
773		}, $files);
774	}
775
776	/**
777	 * Search for files by tag of a given users.
778	 *
779	 * Note that every user can tag files differently.
780	 *
781	 * @param string|int $tag name or tag id
782	 * @param string $userId owner of the tags
783	 * @return ICacheEntry[] file data
784	 */
785	public function searchByTag($tag, $userId) {
786		$sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, ' .
787			'`mimetype`, `mimepart`, `size`, `mtime`, ' .
788			'`encrypted`, `etag`, `permissions`, `checksum` ' .
789			'FROM `*PREFIX*filecache` `file`, ' .
790			'`*PREFIX*vcategory_to_object` `tagmap`, ' .
791			'`*PREFIX*vcategory` `tag` ' .
792			// JOIN filecache to vcategory_to_object
793			'WHERE `file`.`fileid` = `tagmap`.`objid` ' .
794			// JOIN vcategory_to_object to vcategory
795			'AND `tagmap`.`type` = `tag`.`type` ' .
796			'AND `tagmap`.`categoryid` = `tag`.`id` ' .
797			// conditions
798			'AND `file`.`storage` = ? ' .
799			'AND `tag`.`type` = \'files\' ' .
800			'AND `tag`.`uid` = ? ';
801		if (\is_int($tag)) {
802			$sql .= 'AND `tag`.`id` = ? ';
803		} else {
804			$sql .= 'AND `tag`.`category` = ? ';
805		}
806		$result = $this->connection->executeQuery(
807			$sql,
808			[
809				$this->getNumericStorageId(),
810				$userId,
811				$tag
812			]
813		);
814		$files = [];
815		while ($row = $result->fetch()) {
816			$files[] = $row;
817		}
818		return \array_map(function (array $data) {
819			return new CacheEntry($data);
820		}, $files);
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) {
830		$this->calculateFolderSize($path, $data);
831		if ($path !== '') {
832			$parent = \dirname($path);
833			if ($parent === '.' or $parent === '/') {
834				$parent = '';
835			}
836			$this->correctFolderSize($parent);
837		}
838	}
839
840	/**
841	 * calculate the size of a folder and set it in the cache
842	 *
843	 * @param string $path
844	 * @param array $entry (optional) meta data of the folder
845	 * @return int
846	 */
847	public function calculateFolderSize($path, $entry = null) {
848		$totalSize = 0;
849		if ($entry === null or !isset($entry['fileid'])) {
850			$entry = $this->get($path);
851		}
852		if (isset($entry['mimetype']) && $entry['mimetype'] === 'httpd/unix-directory') {
853			$id = $entry['fileid'];
854			$sql = 'SELECT SUM(`size`) AS f1, MIN(`size`) AS f2 ' .
855				'FROM `*PREFIX*filecache` ' .
856				'WHERE `parent` = ? AND `storage` = ?';
857			$result = $this->connection->executeQuery($sql, [$id, $this->getNumericStorageId()]);
858			if ($row = $result->fetch()) {
859				$result->closeCursor();
860				list($sum, $min) = \array_values($row);
861				if ($min === null && $entry['size'] < 0) {
862					// could happen if the folder hasn't been scanned.
863					// we don't have any data, so return the SIZE_NEEDS_SCAN
864					// if the size of the entry is positive it means that the entry has been scanned,
865					// so the folder is empty (no need to be scanned)
866					return IScanner::SIZE_NEEDS_SCAN;
867				}
868				$sum = 0 + $sum;
869				$min = 0 + $min;
870				if ($min === IScanner::SIZE_NEEDS_SCAN || $min === IScanner::SIZE_SHALLOW_SCANNED) {
871					// current folder is shallow scanned
872					$totalSize = IScanner::SIZE_SHALLOW_SCANNED;
873				} else {
874					$totalSize = $sum;
875				}
876				$update = [];
877				if ($entry['size'] !== $totalSize) {
878					$update['size'] = $totalSize;
879				}
880				if (\count($update) > 0) {
881					$this->update($id, $update);
882				}
883			} else {
884				$result->closeCursor();
885			}
886		}
887		return $totalSize;
888	}
889
890	/**
891	 * get all file ids on the files on the storage
892	 *
893	 * @return int[]
894	 */
895	public function getAll() {
896		$sql = 'SELECT `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ?';
897		$result = $this->connection->executeQuery($sql, [$this->getNumericStorageId()]);
898		$ids = [];
899		while ($row = $result->fetch()) {
900			$ids[] = $row['fileid'];
901		}
902		return $ids;
903	}
904
905	/**
906	 * find a folder in the cache which has not been fully scanned
907	 *
908	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
909	 * use the one with the highest id gives the best result with the background scanner, since that is most
910	 * likely the folder where we stopped scanning previously
911	 *
912	 * @return string|bool the path of the folder or false when no folder matched
913	 */
914	public function getIncomplete() {
915		$query = $this->connection->prepare('SELECT `path` FROM `*PREFIX*filecache`'
916			. ' WHERE `storage` = ? AND `size` = ? ORDER BY `fileid` DESC', 1);
917		$query->execute([$this->getNumericStorageId(), IScanner::SIZE_NEEDS_SCAN]);
918		if ($row = $query->fetch()) {
919			return $row['path'];
920		} else {
921			return false;
922		}
923	}
924
925	/**
926	 * get the path of a file on this storage by it's file id
927	 *
928	 * @param int $id the file id of the file or folder to search
929	 * @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
930	 */
931	public function getPathById($id) {
932		$sql = 'SELECT `path` FROM `*PREFIX*filecache` WHERE `fileid` = ? AND `storage` = ?';
933		$result = $this->connection->executeQuery($sql, [$id, $this->getNumericStorageId()]);
934		if ($row = $result->fetch()) {
935			// Oracle stores empty strings as null...
936			if ($row['path'] === null) {
937				return '';
938			}
939			return $row['path'];
940		} else {
941			return null;
942		}
943	}
944
945	/**
946	 * get the storage id of the storage for a file and the internal path of the file
947	 * unlike getPathById this does not limit the search to files on this storage and
948	 * instead does a global search in the cache table
949	 *
950	 * @param int $id
951	 * @deprecated use getPathById() instead
952	 * @return array first element holding the storage id, second the path
953	 */
954	public static function getById($id) {
955		$connection = \OC::$server->getDatabaseConnection();
956		$sql = 'SELECT `storage`, `path` FROM `*PREFIX*filecache` WHERE `fileid` = ?';
957		$result = $connection->executeQuery($sql, [$id]);
958		if ($row = $result->fetch()) {
959			$numericId = $row['storage'];
960			$path = $row['path'];
961		} else {
962			return null;
963		}
964
965		if ($id = Storage::getStorageId($numericId)) {
966			return [$id, $path];
967		} else {
968			return null;
969		}
970	}
971
972	/**
973	 * normalize the given path
974	 *
975	 * @param string $path
976	 * @return string
977	 */
978	public function normalize($path) {
979		return \trim(\OC_Util::normalizeUnicode($path), '/');
980	}
981}
982