You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1002 lines
30 KiB

3 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Andreas Fischer <bantu@owncloud.com>
  6. * @author Ari Selseng <ari@selseng.net>
  7. * @author Artem Kochnev <MrJeos@gmail.com>
  8. * @author Björn Schießle <bjoern@schiessle.org>
  9. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  10. * @author Florin Peter <github@florin-peter.de>
  11. * @author Frédéric Fortier <frederic.fortier@oronospolytechnique.com>
  12. * @author Jens-Christian Fischer <jens-christian.fischer@switch.ch>
  13. * @author Joas Schilling <coding@schilljs.com>
  14. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  15. * @author Lukas Reschke <lukas@statuscode.ch>
  16. * @author Michael Gapczynski <GapczynskiM@gmail.com>
  17. * @author Morris Jobke <hey@morrisjobke.de>
  18. * @author Robin Appelman <robin@icewind.nl>
  19. * @author Robin McCorkell <robin@mccorkell.me.uk>
  20. * @author Roeland Jago Douma <roeland@famdouma.nl>
  21. * @author Vincent Petry <pvince81@owncloud.com>
  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. namespace OC\Files\Cache;
  39. use Doctrine\DBAL\Driver\Statement;
  40. use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
  41. use OCP\DB\QueryBuilder\IQueryBuilder;
  42. use OCP\Files\Cache\CacheInsertEvent;
  43. use OCP\Files\Cache\CacheUpdateEvent;
  44. use OCP\Files\Cache\ICache;
  45. use OCP\Files\Cache\ICacheEntry;
  46. use OCP\Files\FileInfo;
  47. use OCP\Files\IMimeTypeLoader;
  48. use OCP\Files\Search\ISearchQuery;
  49. use OCP\Files\Storage\IStorage;
  50. use OCP\IDBConnection;
  51. /**
  52. * Metadata cache for a storage
  53. *
  54. * The cache stores the metadata for all files and folders in a storage and is kept up to date trough the following mechanisms:
  55. *
  56. * - Scanner: scans the storage and updates the cache where needed
  57. * - Watcher: checks for changes made to the filesystem outside of the ownCloud instance and rescans files and folder when a change is detected
  58. * - Updater: listens to changes made to the filesystem inside of the ownCloud instance and updates the cache where needed
  59. * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
  60. */
  61. class Cache implements ICache {
  62. use MoveFromCacheTrait {
  63. MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
  64. }
  65. /**
  66. * @var array partial data for the cache
  67. */
  68. protected $partial = [];
  69. /**
  70. * @var string
  71. */
  72. protected $storageId;
  73. private $storage;
  74. /**
  75. * @var Storage $storageCache
  76. */
  77. protected $storageCache;
  78. /** @var IMimeTypeLoader */
  79. protected $mimetypeLoader;
  80. /**
  81. * @var IDBConnection
  82. */
  83. protected $connection;
  84. protected $eventDispatcher;
  85. /** @var QuerySearchHelper */
  86. protected $querySearchHelper;
  87. /**
  88. * @param IStorage $storage
  89. */
  90. public function __construct(IStorage $storage) {
  91. $this->storageId = $storage->getId();
  92. $this->storage = $storage;
  93. if (strlen($this->storageId) > 64) {
  94. $this->storageId = md5($this->storageId);
  95. }
  96. $this->storageCache = new Storage($storage);
  97. $this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
  98. $this->connection = \OC::$server->getDatabaseConnection();
  99. $this->eventDispatcher = \OC::$server->getEventDispatcher();
  100. $this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
  101. }
  102. private function getQueryBuilder() {
  103. return new CacheQueryBuilder(
  104. $this->connection,
  105. \OC::$server->getSystemConfig(),
  106. \OC::$server->getLogger(),
  107. $this
  108. );
  109. }
  110. /**
  111. * Get the numeric storage id for this cache's storage
  112. *
  113. * @return int
  114. */
  115. public function getNumericStorageId() {
  116. return $this->storageCache->getNumericId();
  117. }
  118. /**
  119. * get the stored metadata of a file or folder
  120. *
  121. * @param string | int $file either the path of a file or folder or the file id for a file or folder
  122. * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
  123. */
  124. public function get($file) {
  125. $query = $this->getQueryBuilder();
  126. $query->selectFileCache();
  127. if (is_string($file) or $file == '') {
  128. // normalize file
  129. $file = $this->normalize($file);
  130. $query->whereStorageId()
  131. ->wherePath($file);
  132. } else { //file id
  133. $query->whereFileId($file);
  134. }
  135. $data = $query->execute()->fetch();
  136. //merge partial data
  137. if (!$data and is_string($file) and isset($this->partial[$file])) {
  138. return $this->partial[$file];
  139. } elseif (!$data) {
  140. return $data;
  141. } else {
  142. return self::cacheEntryFromData($data, $this->mimetypeLoader);
  143. }
  144. }
  145. /**
  146. * Create a CacheEntry from database row
  147. *
  148. * @param array $data
  149. * @param IMimeTypeLoader $mimetypeLoader
  150. * @return CacheEntry
  151. */
  152. public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
  153. //fix types
  154. $data['fileid'] = (int)$data['fileid'];
  155. $data['parent'] = (int)$data['parent'];
  156. $data['size'] = 0 + $data['size'];
  157. $data['mtime'] = (int)$data['mtime'];
  158. $data['storage_mtime'] = (int)$data['storage_mtime'];
  159. $data['encryptedVersion'] = (int)$data['encrypted'];
  160. $data['encrypted'] = (bool)$data['encrypted'];
  161. $data['storage_id'] = $data['storage'];
  162. $data['storage'] = (int)$data['storage'];
  163. $data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
  164. $data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
  165. if ($data['storage_mtime'] == 0) {
  166. $data['storage_mtime'] = $data['mtime'];
  167. }
  168. $data['permissions'] = (int)$data['permissions'];
  169. if (isset($data['creation_time'])) {
  170. $data['creation_time'] = (int) $data['creation_time'];
  171. }
  172. if (isset($data['upload_time'])) {
  173. $data['upload_time'] = (int) $data['upload_time'];
  174. }
  175. return new CacheEntry($data);
  176. }
  177. /**
  178. * get the metadata of all files stored in $folder
  179. *
  180. * @param string $folder
  181. * @return ICacheEntry[]
  182. */
  183. public function getFolderContents($folder) {
  184. $fileId = $this->getId($folder);
  185. return $this->getFolderContentsById($fileId);
  186. }
  187. /**
  188. * get the metadata of all files stored in $folder
  189. *
  190. * @param int $fileId the file id of the folder
  191. * @return ICacheEntry[]
  192. */
  193. public function getFolderContentsById($fileId) {
  194. if ($fileId > -1) {
  195. $query = $this->getQueryBuilder();
  196. $query->selectFileCache()
  197. ->whereParent($fileId)
  198. ->orderBy('name', 'ASC');
  199. $files = $query->execute()->fetchAll();
  200. return array_map(function (array $data) {
  201. return self::cacheEntryFromData($data, $this->mimetypeLoader);
  202. }, $files);
  203. }
  204. return [];
  205. }
  206. /**
  207. * insert or update meta data for a file or folder
  208. *
  209. * @param string $file
  210. * @param array $data
  211. *
  212. * @return int file id
  213. * @throws \RuntimeException
  214. */
  215. public function put($file, array $data) {
  216. if (($id = $this->getId($file)) > -1) {
  217. $this->update($id, $data);
  218. return $id;
  219. } else {
  220. return $this->insert($file, $data);
  221. }
  222. }
  223. /**
  224. * insert meta data for a new file or folder
  225. *
  226. * @param string $file
  227. * @param array $data
  228. *
  229. * @return int file id
  230. * @throws \RuntimeException
  231. */
  232. public function insert($file, array $data) {
  233. // normalize file
  234. $file = $this->normalize($file);
  235. if (isset($this->partial[$file])) { //add any saved partial data
  236. $data = array_merge($this->partial[$file], $data);
  237. unset($this->partial[$file]);
  238. }
  239. $requiredFields = ['size', 'mtime', 'mimetype'];
  240. foreach ($requiredFields as $field) {
  241. if (!isset($data[$field])) { //data not complete save as partial and return
  242. $this->partial[$file] = $data;
  243. return -1;
  244. }
  245. }
  246. $data['path'] = $file;
  247. if (!isset($data['parent'])) {
  248. $data['parent'] = $this->getParentId($file);
  249. }
  250. $data['name'] = basename($file);
  251. [$values, $extensionValues] = $this->normalizeData($data);
  252. $values['storage'] = $this->getNumericStorageId();
  253. try {
  254. $builder = $this->connection->getQueryBuilder();
  255. $builder->insert('filecache');
  256. foreach ($values as $column => $value) {
  257. $builder->setValue($column, $builder->createNamedParameter($value));
  258. }
  259. if ($builder->execute()) {
  260. $fileId = $builder->getLastInsertId();
  261. if (count($extensionValues)) {
  262. $query = $this->getQueryBuilder();
  263. $query->insert('filecache_extended');
  264. $query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
  265. foreach ($extensionValues as $column => $value) {
  266. $query->setValue($column, $query->createNamedParameter($value));
  267. }
  268. $query->execute();
  269. }
  270. $this->eventDispatcher->dispatch(CacheInsertEvent::class, new CacheInsertEvent($this->storage, $file, $fileId));
  271. return $fileId;
  272. }
  273. } catch (UniqueConstraintViolationException $e) {
  274. // entry exists already
  275. if ($this->connection->inTransaction()) {
  276. $this->connection->commit();
  277. $this->connection->beginTransaction();
  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 but could also not be selected with getId() in order to perform an update. Please try again.');
  286. }
  287. }
  288. /**
  289. * update the metadata of an existing file or folder in the cache
  290. *
  291. * @param int $id the fileid of the existing file or folder
  292. * @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
  293. */
  294. public function update($id, array $data) {
  295. if (isset($data['path'])) {
  296. // normalize path
  297. $data['path'] = $this->normalize($data['path']);
  298. }
  299. if (isset($data['name'])) {
  300. // normalize path
  301. $data['name'] = $this->normalize($data['name']);
  302. }
  303. [$values, $extensionValues] = $this->normalizeData($data);
  304. if (count($values)) {
  305. $query = $this->getQueryBuilder();
  306. $query->update('filecache')
  307. ->whereFileId($id)
  308. ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
  309. return $query->expr()->orX(
  310. $query->expr()->neq($key, $query->createNamedParameter($value)),
  311. $query->expr()->isNull($key)
  312. );
  313. }, array_keys($values), array_values($values))));
  314. foreach ($values as $key => $value) {
  315. $query->set($key, $query->createNamedParameter($value));
  316. }
  317. $query->execute();
  318. }
  319. if (count($extensionValues)) {
  320. try {
  321. $query = $this->getQueryBuilder();
  322. $query->insert('filecache_extended');
  323. $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
  324. foreach ($extensionValues as $column => $value) {
  325. $query->setValue($column, $query->createNamedParameter($value));
  326. }
  327. $query->execute();
  328. } catch (UniqueConstraintViolationException $e) {
  329. $query = $this->getQueryBuilder();
  330. $query->update('filecache_extended')
  331. ->whereFileId($id)
  332. ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
  333. return $query->expr()->orX(
  334. $query->expr()->neq($key, $query->createNamedParameter($value)),
  335. $query->expr()->isNull($key)
  336. );
  337. }, array_keys($extensionValues), array_values($extensionValues))));
  338. foreach ($extensionValues as $key => $value) {
  339. $query->set($key, $query->createNamedParameter($value));
  340. }
  341. $query->execute();
  342. }
  343. }
  344. $path = $this->getPathById($id);
  345. // path can still be null if the file doesn't exist
  346. if ($path !== null) {
  347. $this->eventDispatcher->dispatch(CacheUpdateEvent::class, new CacheUpdateEvent($this->storage, $path, $id));
  348. }
  349. }
  350. /**
  351. * extract query parts and params array from data array
  352. *
  353. * @param array $data
  354. * @return array
  355. */
  356. protected function normalizeData(array $data): array {
  357. $fields = [
  358. 'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
  359. 'etag', 'permissions', 'checksum', 'storage'];
  360. $extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
  361. $doNotCopyStorageMTime = false;
  362. if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
  363. // this horrific magic tells it to not copy storage_mtime to mtime
  364. unset($data['mtime']);
  365. $doNotCopyStorageMTime = true;
  366. }
  367. $params = [];
  368. $extensionParams = [];
  369. foreach ($data as $name => $value) {
  370. if (array_search($name, $fields) !== false) {
  371. if ($name === 'path') {
  372. $params['path_hash'] = md5($value);
  373. } elseif ($name === 'mimetype') {
  374. $params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
  375. $value = $this->mimetypeLoader->getId($value);
  376. } elseif ($name === 'storage_mtime') {
  377. if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
  378. $params['mtime'] = $value;
  379. }
  380. } elseif ($name === 'encrypted') {
  381. if (isset($data['encryptedVersion'])) {
  382. $value = $data['encryptedVersion'];
  383. } else {
  384. // Boolean to integer conversion
  385. $value = $value ? 1 : 0;
  386. }
  387. }
  388. $params[$name] = $value;
  389. }
  390. if (array_search($name, $extensionFields) !== false) {
  391. $extensionParams[$name] = $value;
  392. }
  393. }
  394. return [$params, array_filter($extensionParams)];
  395. }
  396. /**
  397. * get the file id for a file
  398. *
  399. * 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
  400. *
  401. * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
  402. *
  403. * @param string $file
  404. * @return int
  405. */
  406. public function getId($file) {
  407. // normalize file
  408. $file = $this->normalize($file);
  409. $query = $this->getQueryBuilder();
  410. $query->select('fileid')
  411. ->from('filecache')
  412. ->whereStorageId()
  413. ->wherePath($file);
  414. $id = $query->execute()->fetchColumn();
  415. return $id === false ? -1 : (int)$id;
  416. }
  417. /**
  418. * get the id of the parent folder of a file
  419. *
  420. * @param string $file
  421. * @return int
  422. */
  423. public function getParentId($file) {
  424. if ($file === '') {
  425. return -1;
  426. } else {
  427. $parent = $this->getParentPath($file);
  428. return (int)$this->getId($parent);
  429. }
  430. }
  431. private function getParentPath($path) {
  432. $parent = dirname($path);
  433. if ($parent === '.') {
  434. $parent = '';
  435. }
  436. return $parent;
  437. }
  438. /**
  439. * check if a file is available in the cache
  440. *
  441. * @param string $file
  442. * @return bool
  443. */
  444. public function inCache($file) {
  445. return $this->getId($file) != -1;
  446. }
  447. /**
  448. * remove a file or folder from the cache
  449. *
  450. * when removing a folder from the cache all files and folders inside the folder will be removed as well
  451. *
  452. * @param string $file
  453. */
  454. public function remove($file) {
  455. $entry = $this->get($file);
  456. if ($entry) {
  457. $query = $this->getQueryBuilder();
  458. $query->delete('filecache')
  459. ->whereFileId($entry->getId());
  460. $query->execute();
  461. $query = $this->getQueryBuilder();
  462. $query->delete('filecache_extended')
  463. ->whereFileId($entry->getId());
  464. $query->execute();
  465. if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
  466. $this->removeChildren($entry);
  467. }
  468. }
  469. }
  470. /**
  471. * Get all sub folders of a folder
  472. *
  473. * @param ICacheEntry $entry the cache entry of the folder to get the subfolders for
  474. * @return ICacheEntry[] the cache entries for the subfolders
  475. */
  476. private function getSubFolders(ICacheEntry $entry) {
  477. $children = $this->getFolderContentsById($entry->getId());
  478. return array_filter($children, function ($child) {
  479. return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
  480. });
  481. }
  482. /**
  483. * Recursively remove all children of a folder
  484. *
  485. * @param ICacheEntry $entry the cache entry of the folder to remove the children of
  486. * @throws \OC\DatabaseException
  487. */
  488. private function removeChildren(ICacheEntry $entry) {
  489. $parentIds = [$entry->getId()];
  490. $queue = [$entry->getId()];
  491. // we walk depth first trough the file tree, removing all filecache_extended attributes while we walk
  492. // and collecting all folder ids to later use to delete the filecache entries
  493. while ($entryId = array_pop($queue)) {
  494. $children = $this->getFolderContentsById($entryId);
  495. $childIds = array_map(function (ICacheEntry $cacheEntry) {
  496. return $cacheEntry->getId();
  497. }, $children);
  498. $query = $this->getQueryBuilder();
  499. $query->delete('filecache_extended')
  500. ->where($query->expr()->in('fileid', $query->createNamedParameter($childIds, IQueryBuilder::PARAM_INT_ARRAY)));
  501. $query->execute();
  502. /** @var ICacheEntry[] $childFolders */
  503. $childFolders = array_filter($children, function ($child) {
  504. return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
  505. });
  506. foreach ($childFolders as $folder) {
  507. $parentIds[] = $folder->getId();
  508. $queue[] = $folder->getId();
  509. }
  510. }
  511. $query = $this->getQueryBuilder();
  512. $query->delete('filecache')
  513. ->whereParentIn($parentIds);
  514. $query->execute();
  515. }
  516. /**
  517. * Move a file or folder in the cache
  518. *
  519. * @param string $source
  520. * @param string $target
  521. */
  522. public function move($source, $target) {
  523. $this->moveFromCache($this, $source, $target);
  524. }
  525. /**
  526. * Get the storage id and path needed for a move
  527. *
  528. * @param string $path
  529. * @return array [$storageId, $internalPath]
  530. */
  531. protected function getMoveInfo($path) {
  532. return [$this->getNumericStorageId(), $path];
  533. }
  534. /**
  535. * Move a file or folder in the cache
  536. *
  537. * @param \OCP\Files\Cache\ICache $sourceCache
  538. * @param string $sourcePath
  539. * @param string $targetPath
  540. * @throws \OC\DatabaseException
  541. * @throws \Exception if the given storages have an invalid id
  542. */
  543. public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
  544. if ($sourceCache instanceof Cache) {
  545. // normalize source and target
  546. $sourcePath = $this->normalize($sourcePath);
  547. $targetPath = $this->normalize($targetPath);
  548. $sourceData = $sourceCache->get($sourcePath);
  549. $sourceId = $sourceData['fileid'];
  550. $newParentId = $this->getParentId($targetPath);
  551. [$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
  552. [$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
  553. if (is_null($sourceStorageId) || $sourceStorageId === false) {
  554. throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
  555. }
  556. if (is_null($targetStorageId) || $targetStorageId === false) {
  557. throw new \Exception('Invalid target storage id: ' . $targetStorageId);
  558. }
  559. $this->connection->beginTransaction();
  560. if ($sourceData['mimetype'] === 'httpd/unix-directory') {
  561. //update all child entries
  562. $sourceLength = mb_strlen($sourcePath);
  563. $query = $this->connection->getQueryBuilder();
  564. $fun = $query->func();
  565. $newPathFunction = $fun->concat(
  566. $query->createNamedParameter($targetPath),
  567. $fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
  568. );
  569. $query->update('filecache')
  570. ->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
  571. ->set('path_hash', $fun->md5($newPathFunction))
  572. ->set('path', $newPathFunction)
  573. ->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
  574. ->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
  575. try {
  576. $query->execute();
  577. } catch (\OC\DatabaseException $e) {
  578. $this->connection->rollBack();
  579. throw $e;
  580. }
  581. }
  582. $query = $this->getQueryBuilder();
  583. $query->update('filecache')
  584. ->set('storage', $query->createNamedParameter($targetStorageId))
  585. ->set('path', $query->createNamedParameter($targetPath))
  586. ->set('path_hash', $query->createNamedParameter(md5($targetPath)))
  587. ->set('name', $query->createNamedParameter(basename($targetPath)))
  588. ->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
  589. ->whereFileId($sourceId);
  590. $query->execute();
  591. $this->connection->commit();
  592. } else {
  593. $this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
  594. }
  595. }
  596. /**
  597. * remove all entries for files that are stored on the storage from the cache
  598. */
  599. public function clear() {
  600. $query = $this->getQueryBuilder();
  601. $query->delete('filecache')
  602. ->whereStorageId();
  603. $query->execute();
  604. $query = $this->connection->getQueryBuilder();
  605. $query->delete('storages')
  606. ->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
  607. $query->execute();
  608. }
  609. /**
  610. * Get the scan status of a file
  611. *
  612. * - Cache::NOT_FOUND: File is not in the cache
  613. * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
  614. * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
  615. * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
  616. *
  617. * @param string $file
  618. *
  619. * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
  620. */
  621. public function getStatus($file) {
  622. // normalize file
  623. $file = $this->normalize($file);
  624. $query = $this->getQueryBuilder();
  625. $query->select('size')
  626. ->from('filecache')
  627. ->whereStorageId()
  628. ->wherePath($file);
  629. $size = $query->execute()->fetchColumn();
  630. if ($size !== false) {
  631. if ((int)$size === -1) {
  632. return self::SHALLOW;
  633. } else {
  634. return self::COMPLETE;
  635. }
  636. } else {
  637. if (isset($this->partial[$file])) {
  638. return self::PARTIAL;
  639. } else {
  640. return self::NOT_FOUND;
  641. }
  642. }
  643. }
  644. /**
  645. * search for files matching $pattern
  646. *
  647. * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
  648. * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
  649. */
  650. public function search($pattern) {
  651. // normalize pattern
  652. $pattern = $this->normalize($pattern);
  653. if ($pattern === '%%') {
  654. return [];
  655. }
  656. $query = $this->getQueryBuilder();
  657. $query->selectFileCache()
  658. ->whereStorageId()
  659. ->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
  660. return array_map(function (array $data) {
  661. return self::cacheEntryFromData($data, $this->mimetypeLoader);
  662. }, $query->execute()->fetchAll());
  663. }
  664. /**
  665. * @param Statement $result
  666. * @return CacheEntry[]
  667. */
  668. private function searchResultToCacheEntries(Statement $result) {
  669. $files = $result->fetchAll();
  670. return array_map(function (array $data) {
  671. return self::cacheEntryFromData($data, $this->mimetypeLoader);
  672. }, $files);
  673. }
  674. /**
  675. * search for files by mimetype
  676. *
  677. * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
  678. * where it will search for all mimetypes in the group ('image/*')
  679. * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
  680. */
  681. public function searchByMime($mimetype) {
  682. $mimeId = $this->mimetypeLoader->getId($mimetype);
  683. $query = $this->getQueryBuilder();
  684. $query->selectFileCache()
  685. ->whereStorageId();
  686. if (strpos($mimetype, '/')) {
  687. $query->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
  688. } else {
  689. $query->andWhere($query->expr()->eq('mimepart', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
  690. }
  691. return array_map(function (array $data) {
  692. return self::cacheEntryFromData($data, $this->mimetypeLoader);
  693. }, $query->execute()->fetchAll());
  694. }
  695. public function searchQuery(ISearchQuery $searchQuery) {
  696. $builder = $this->getQueryBuilder();
  697. $query = $builder->selectFileCache('file');
  698. $query->whereStorageId();
  699. if ($this->querySearchHelper->shouldJoinTags($searchQuery->getSearchOperation())) {
  700. $query
  701. ->innerJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid'))
  702. ->innerJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX(
  703. $builder->expr()->eq('tagmap.type', 'tag.type'),
  704. $builder->expr()->eq('tagmap.categoryid', 'tag.id')
  705. ))
  706. ->andWhere($builder->expr()->eq('tag.type', $builder->createNamedParameter('files')))
  707. ->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($searchQuery->getUser()->getUID())));
  708. }
  709. $searchExpr = $this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation());
  710. if ($searchExpr) {
  711. $query->andWhere($searchExpr);
  712. }
  713. if ($searchQuery->limitToHome() && ($this instanceof HomeCache)) {
  714. $query->andWhere($builder->expr()->like('path', $query->expr()->literal('files/%')));
  715. }
  716. $this->querySearchHelper->addSearchOrdersToQuery($query, $searchQuery->getOrder());
  717. if ($searchQuery->getLimit()) {
  718. $query->setMaxResults($searchQuery->getLimit());
  719. }
  720. if ($searchQuery->getOffset()) {
  721. $query->setFirstResult($searchQuery->getOffset());
  722. }
  723. $result = $query->execute();
  724. return $this->searchResultToCacheEntries($result);
  725. }
  726. /**
  727. * Re-calculate the folder size and the size of all parent folders
  728. *
  729. * @param string|boolean $path
  730. * @param array $data (optional) meta data of the folder
  731. */
  732. public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
  733. $this->calculateFolderSize($path, $data);
  734. if ($path !== '') {
  735. $parent = dirname($path);
  736. if ($parent === '.' or $parent === '/') {
  737. $parent = '';
  738. }
  739. if ($isBackgroundScan) {
  740. $parentData = $this->get($parent);
  741. if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
  742. $this->correctFolderSize($parent, $parentData, $isBackgroundScan);
  743. }
  744. } else {
  745. $this->correctFolderSize($parent);
  746. }
  747. }
  748. }
  749. /**
  750. * get the incomplete count that shares parent $folder
  751. *
  752. * @param int $fileId the file id of the folder
  753. * @return int
  754. */
  755. public function getIncompleteChildrenCount($fileId) {
  756. if ($fileId > -1) {
  757. $query = $this->getQueryBuilder();
  758. $query->select($query->func()->count())
  759. ->from('filecache')
  760. ->whereParent($fileId)
  761. ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
  762. return (int)$query->execute()->fetchColumn();
  763. }
  764. return -1;
  765. }
  766. /**
  767. * calculate the size of a folder and set it in the cache
  768. *
  769. * @param string $path
  770. * @param array $entry (optional) meta data of the folder
  771. * @return int
  772. */
  773. public function calculateFolderSize($path, $entry = null) {
  774. $totalSize = 0;
  775. if (is_null($entry) or !isset($entry['fileid'])) {
  776. $entry = $this->get($path);
  777. }
  778. if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
  779. $id = $entry['fileid'];
  780. $query = $this->getQueryBuilder();
  781. $query->selectAlias($query->func()->sum('size'), 'f1')
  782. ->selectAlias($query->func()->min('size'), 'f2')
  783. ->from('filecache')
  784. ->whereStorageId()
  785. ->whereParent($id);
  786. if ($row = $query->execute()->fetch()) {
  787. [$sum, $min] = array_values($row);
  788. $sum = 0 + $sum;
  789. $min = 0 + $min;
  790. if ($min === -1) {
  791. $totalSize = $min;
  792. } else {
  793. $totalSize = $sum;
  794. }
  795. if ($entry['size'] !== $totalSize) {
  796. $this->update($id, ['size' => $totalSize]);
  797. }
  798. }
  799. }
  800. return $totalSize;
  801. }
  802. /**
  803. * get all file ids on the files on the storage
  804. *
  805. * @return int[]
  806. */
  807. public function getAll() {
  808. $query = $this->getQueryBuilder();
  809. $query->select('fileid')
  810. ->from('filecache')
  811. ->whereStorageId();
  812. return array_map(function ($id) {
  813. return (int)$id;
  814. }, $query->execute()->fetchAll(\PDO::FETCH_COLUMN));
  815. }
  816. /**
  817. * find a folder in the cache which has not been fully scanned
  818. *
  819. * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
  820. * use the one with the highest id gives the best result with the background scanner, since that is most
  821. * likely the folder where we stopped scanning previously
  822. *
  823. * @return string|bool the path of the folder or false when no folder matched
  824. */
  825. public function getIncomplete() {
  826. $query = $this->getQueryBuilder();
  827. $query->select('path')
  828. ->from('filecache')
  829. ->whereStorageId()
  830. ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
  831. ->orderBy('fileid', 'DESC');
  832. return $query->execute()->fetchColumn();
  833. }
  834. /**
  835. * get the path of a file on this storage by it's file id
  836. *
  837. * @param int $id the file id of the file or folder to search
  838. * @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
  839. */
  840. public function getPathById($id) {
  841. $query = $this->getQueryBuilder();
  842. $query->select('path')
  843. ->from('filecache')
  844. ->whereStorageId()
  845. ->whereFileId($id);
  846. $path = $query->execute()->fetchColumn();
  847. return $path === false ? null : $path;
  848. }
  849. /**
  850. * get the storage id of the storage for a file and the internal path of the file
  851. * unlike getPathById this does not limit the search to files on this storage and
  852. * instead does a global search in the cache table
  853. *
  854. * @param int $id
  855. * @return array first element holding the storage id, second the path
  856. * @deprecated use getPathById() instead
  857. */
  858. public static function getById($id) {
  859. $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
  860. $query->select('path', 'storage')
  861. ->from('filecache')
  862. ->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
  863. if ($row = $query->execute()->fetch()) {
  864. $numericId = $row['storage'];
  865. $path = $row['path'];
  866. } else {
  867. return null;
  868. }
  869. if ($id = Storage::getStorageId($numericId)) {
  870. return [$id, $path];
  871. } else {
  872. return null;
  873. }
  874. }
  875. /**
  876. * normalize the given path
  877. *
  878. * @param string $path
  879. * @return string
  880. */
  881. public function normalize($path) {
  882. return trim(\OC_Util::normalizeUnicode($path), '/');
  883. }
  884. }