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.

817 lines
25 KiB

10 years ago
10 years ago
13 years ago
13 years ago
12 years ago
  1. <?php
  2. /**
  3. * @author Andreas Fischer <bantu@owncloud.com>
  4. * @author Björn Schießle <schiessle@owncloud.com>
  5. * @author Florin Peter <github@florin-peter.de>
  6. * @author Jens-Christian Fischer <jens-christian.fischer@switch.ch>
  7. * @author Joas Schilling <nickvergessen@owncloud.com>
  8. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  9. * @author Michael Gapczynski <GapczynskiM@gmail.com>
  10. * @author Morris Jobke <hey@morrisjobke.de>
  11. * @author Robin Appelman <icewind@owncloud.com>
  12. * @author Robin McCorkell <robin@mccorkell.me.uk>
  13. * @author TheSFReader <TheSFReader@gmail.com>
  14. * @author Thomas Müller <thomas.mueller@tmit.eu>
  15. * @author Vincent Petry <pvince81@owncloud.com>
  16. *
  17. * @copyright Copyright (c) 2016, ownCloud, Inc.
  18. * @license AGPL-3.0
  19. *
  20. * This code is free software: you can redistribute it and/or modify
  21. * it under the terms of the GNU Affero General Public License, version 3,
  22. * as published by the Free Software Foundation.
  23. *
  24. * This program is distributed in the hope that it will be useful,
  25. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  26. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  27. * GNU Affero General Public License for more details.
  28. *
  29. * You should have received a copy of the GNU Affero General Public License, version 3,
  30. * along with this program. If not, see <http://www.gnu.org/licenses/>
  31. *
  32. */
  33. namespace OC\Files\Cache;
  34. use OCP\Files\Cache\ICache;
  35. use OCP\Files\Cache\ICacheEntry;
  36. use \OCP\Files\IMimeTypeLoader;
  37. use OCP\IDBConnection;
  38. /**
  39. * Metadata cache for a storage
  40. *
  41. * The cache stores the metadata for all files and folders in a storage and is kept up to date trough the following mechanisms:
  42. *
  43. * - Scanner: scans the storage and updates the cache where needed
  44. * - Watcher: checks for changes made to the filesystem outside of the ownCloud instance and rescans files and folder when a change is detected
  45. * - Updater: listens to changes made to the filesystem inside of the ownCloud instance and updates the cache where needed
  46. * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
  47. */
  48. class Cache implements ICache {
  49. use MoveFromCacheTrait {
  50. MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
  51. }
  52. /**
  53. * @var array partial data for the cache
  54. */
  55. protected $partial = array();
  56. /**
  57. * @var string
  58. */
  59. protected $storageId;
  60. /**
  61. * @var Storage $storageCache
  62. */
  63. protected $storageCache;
  64. /** @var IMimeTypeLoader */
  65. protected $mimetypeLoader;
  66. /**
  67. * @var IDBConnection
  68. */
  69. protected $connection;
  70. /**
  71. * @param \OC\Files\Storage\Storage|string $storage
  72. */
  73. public function __construct($storage) {
  74. if ($storage instanceof \OC\Files\Storage\Storage) {
  75. $this->storageId = $storage->getId();
  76. } else {
  77. $this->storageId = $storage;
  78. }
  79. if (strlen($this->storageId) > 64) {
  80. $this->storageId = md5($this->storageId);
  81. }
  82. $this->storageCache = new Storage($storage);
  83. $this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
  84. $this->connection = \OC::$server->getDatabaseConnection();
  85. }
  86. /**
  87. * Get the numeric storage id for this cache's storage
  88. *
  89. * @return int
  90. */
  91. public function getNumericStorageId() {
  92. return $this->storageCache->getNumericId();
  93. }
  94. /**
  95. * get the stored metadata of a file or folder
  96. *
  97. * @param string | int $file either the path of a file or folder or the file id for a file or folder
  98. * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
  99. */
  100. public function get($file) {
  101. if (is_string($file) or $file == '') {
  102. // normalize file
  103. $file = $this->normalize($file);
  104. $where = 'WHERE `storage` = ? AND `path_hash` = ?';
  105. $params = array($this->getNumericStorageId(), md5($file));
  106. } else { //file id
  107. $where = 'WHERE `fileid` = ?';
  108. $params = array($file);
  109. }
  110. $sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`,
  111. `storage_mtime`, `encrypted`, `etag`, `permissions`, `checksum`
  112. FROM `*PREFIX*filecache` ' . $where;
  113. $result = $this->connection->executeQuery($sql, $params);
  114. $data = $result->fetch();
  115. //FIXME hide this HACK in the next database layer, or just use doctrine and get rid of MDB2 and PDO
  116. //PDO returns false, MDB2 returns null, oracle always uses MDB2, so convert null to false
  117. if ($data === null) {
  118. $data = false;
  119. }
  120. //merge partial data
  121. if (!$data and is_string($file)) {
  122. if (isset($this->partial[$file])) {
  123. $data = $this->partial[$file];
  124. }
  125. return $data;
  126. } else {
  127. //fix types
  128. $data['fileid'] = (int)$data['fileid'];
  129. $data['parent'] = (int)$data['parent'];
  130. $data['size'] = 0 + $data['size'];
  131. $data['mtime'] = (int)$data['mtime'];
  132. $data['storage_mtime'] = (int)$data['storage_mtime'];
  133. $data['encrypted'] = (bool)$data['encrypted'];
  134. $data['storage'] = $this->storageId;
  135. $data['mimetype'] = $this->mimetypeLoader->getMimetypeById($data['mimetype']);
  136. $data['mimepart'] = $this->mimetypeLoader->getMimetypeById($data['mimepart']);
  137. if ($data['storage_mtime'] == 0) {
  138. $data['storage_mtime'] = $data['mtime'];
  139. }
  140. $data['permissions'] = (int)$data['permissions'];
  141. return new CacheEntry($data);
  142. }
  143. }
  144. /**
  145. * get the metadata of all files stored in $folder
  146. *
  147. * @param string $folder
  148. * @return ICacheEntry[]
  149. */
  150. public function getFolderContents($folder) {
  151. $fileId = $this->getId($folder);
  152. return $this->getFolderContentsById($fileId);
  153. }
  154. /**
  155. * get the metadata of all files stored in $folder
  156. *
  157. * @param int $fileId the file id of the folder
  158. * @return ICacheEntry[]
  159. */
  160. public function getFolderContentsById($fileId) {
  161. if ($fileId > -1) {
  162. $sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`,
  163. `storage_mtime`, `encrypted`, `etag`, `permissions`, `checksum`
  164. FROM `*PREFIX*filecache` WHERE `parent` = ? ORDER BY `name` ASC';
  165. $result = $this->connection->executeQuery($sql, [$fileId]);
  166. $files = $result->fetchAll();
  167. foreach ($files as &$file) {
  168. $file['mimetype'] = $this->mimetypeLoader->getMimetypeById($file['mimetype']);
  169. $file['mimepart'] = $this->mimetypeLoader->getMimetypeById($file['mimepart']);
  170. if ($file['storage_mtime'] == 0) {
  171. $file['storage_mtime'] = $file['mtime'];
  172. }
  173. $file['permissions'] = (int)$file['permissions'];
  174. $file['mtime'] = (int)$file['mtime'];
  175. $file['storage_mtime'] = (int)$file['storage_mtime'];
  176. $file['size'] = 0 + $file['size'];
  177. }
  178. return array_map(function (array $data) {
  179. return new CacheEntry($data);
  180. }, $files);
  181. } else {
  182. return array();
  183. }
  184. }
  185. /**
  186. * store meta data for a file or folder
  187. *
  188. * @param string $file
  189. * @param array $data
  190. *
  191. * @return int file id
  192. * @throws \RuntimeException
  193. */
  194. public function put($file, array $data) {
  195. if (($id = $this->getId($file)) > -1) {
  196. $this->update($id, $data);
  197. return $id;
  198. } else {
  199. // normalize file
  200. $file = $this->normalize($file);
  201. if (isset($this->partial[$file])) { //add any saved partial data
  202. $data = array_merge($this->partial[$file], $data);
  203. unset($this->partial[$file]);
  204. }
  205. $requiredFields = array('size', 'mtime', 'mimetype');
  206. foreach ($requiredFields as $field) {
  207. if (!isset($data[$field])) { //data not complete save as partial and return
  208. $this->partial[$file] = $data;
  209. return -1;
  210. }
  211. }
  212. $data['path'] = $file;
  213. $data['parent'] = $this->getParentId($file);
  214. $data['name'] = \OC_Util::basename($file);
  215. list($queryParts, $params) = $this->buildParts($data);
  216. $queryParts[] = '`storage`';
  217. $params[] = $this->getNumericStorageId();
  218. $queryParts = array_map(function ($item) {
  219. return trim($item, "`");
  220. }, $queryParts);
  221. $values = array_combine($queryParts, $params);
  222. if (\OC::$server->getDatabaseConnection()->insertIfNotExist('*PREFIX*filecache', $values, [
  223. 'storage',
  224. 'path_hash',
  225. ])
  226. ) {
  227. return (int)$this->connection->lastInsertId('*PREFIX*filecache');
  228. }
  229. // The file was created in the mean time
  230. if (($id = $this->getId($file)) > -1) {
  231. $this->update($id, $data);
  232. return $id;
  233. } else {
  234. throw new \RuntimeException('File entry could not be inserted with insertIfNotExist() but could also not be selected with getId() in order to perform an update. Please try again.');
  235. }
  236. }
  237. }
  238. /**
  239. * update the metadata of an existing file or folder in the cache
  240. *
  241. * @param int $id the fileid of the existing file or folder
  242. * @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
  243. */
  244. public function update($id, array $data) {
  245. if (isset($data['path'])) {
  246. // normalize path
  247. $data['path'] = $this->normalize($data['path']);
  248. }
  249. if (isset($data['name'])) {
  250. // normalize path
  251. $data['name'] = $this->normalize($data['name']);
  252. }
  253. list($queryParts, $params) = $this->buildParts($data);
  254. // duplicate $params because we need the parts twice in the SQL statement
  255. // once for the SET part, once in the WHERE clause
  256. $params = array_merge($params, $params);
  257. $params[] = $id;
  258. // don't update if the data we try to set is the same as the one in the record
  259. // some databases (Postgres) don't like superfluous updates
  260. $sql = 'UPDATE `*PREFIX*filecache` SET ' . implode(' = ?, ', $queryParts) . '=? ' .
  261. 'WHERE (' .
  262. implode(' <> ? OR ', $queryParts) . ' <> ? OR ' .
  263. implode(' IS NULL OR ', $queryParts) . ' IS NULL' .
  264. ') AND `fileid` = ? ';
  265. $this->connection->executeQuery($sql, $params);
  266. }
  267. /**
  268. * extract query parts and params array from data array
  269. *
  270. * @param array $data
  271. * @return array [$queryParts, $params]
  272. * $queryParts: string[], the (escaped) column names to be set in the query
  273. * $params: mixed[], the new values for the columns, to be passed as params to the query
  274. */
  275. protected function buildParts(array $data) {
  276. $fields = array(
  277. 'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
  278. 'etag', 'permissions', 'checksum');
  279. $doNotCopyStorageMTime = false;
  280. if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
  281. // this horrific magic tells it to not copy storage_mtime to mtime
  282. unset($data['mtime']);
  283. $doNotCopyStorageMTime = true;
  284. }
  285. $params = array();
  286. $queryParts = array();
  287. foreach ($data as $name => $value) {
  288. if (array_search($name, $fields) !== false) {
  289. if ($name === 'path') {
  290. $params[] = md5($value);
  291. $queryParts[] = '`path_hash`';
  292. } elseif ($name === 'mimetype') {
  293. $params[] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
  294. $queryParts[] = '`mimepart`';
  295. $value = $this->mimetypeLoader->getId($value);
  296. } elseif ($name === 'storage_mtime') {
  297. if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
  298. $params[] = $value;
  299. $queryParts[] = '`mtime`';
  300. }
  301. } elseif ($name === 'encrypted') {
  302. // Boolean to integer conversion
  303. $value = $value ? 1 : 0;
  304. }
  305. $params[] = $value;
  306. $queryParts[] = '`' . $name . '`';
  307. }
  308. }
  309. return array($queryParts, $params);
  310. }
  311. /**
  312. * get the file id for a file
  313. *
  314. * 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
  315. *
  316. * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
  317. *
  318. * @param string $file
  319. * @return int
  320. */
  321. public function getId($file) {
  322. // normalize file
  323. $file = $this->normalize($file);
  324. $pathHash = md5($file);
  325. $sql = 'SELECT `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path_hash` = ?';
  326. $result = $this->connection->executeQuery($sql, array($this->getNumericStorageId(), $pathHash));
  327. if ($row = $result->fetch()) {
  328. return $row['fileid'];
  329. } else {
  330. return -1;
  331. }
  332. }
  333. /**
  334. * get the id of the parent folder of a file
  335. *
  336. * @param string $file
  337. * @return int
  338. */
  339. public function getParentId($file) {
  340. if ($file === '') {
  341. return -1;
  342. } else {
  343. $parent = $this->getParentPath($file);
  344. return (int)$this->getId($parent);
  345. }
  346. }
  347. private function getParentPath($path) {
  348. $parent = dirname($path);
  349. if ($parent === '.') {
  350. $parent = '';
  351. }
  352. return $parent;
  353. }
  354. /**
  355. * check if a file is available in the cache
  356. *
  357. * @param string $file
  358. * @return bool
  359. */
  360. public function inCache($file) {
  361. return $this->getId($file) != -1;
  362. }
  363. /**
  364. * remove a file or folder from the cache
  365. *
  366. * when removing a folder from the cache all files and folders inside the folder will be removed as well
  367. *
  368. * @param string $file
  369. */
  370. public function remove($file) {
  371. $entry = $this->get($file);
  372. $sql = 'DELETE FROM `*PREFIX*filecache` WHERE `fileid` = ?';
  373. $this->connection->executeQuery($sql, array($entry['fileid']));
  374. if ($entry['mimetype'] === 'httpd/unix-directory') {
  375. $this->removeChildren($entry);
  376. }
  377. }
  378. /**
  379. * Get all sub folders of a folder
  380. *
  381. * @param array $entry the cache entry of the folder to get the subfolders for
  382. * @return array[] the cache entries for the subfolders
  383. */
  384. private function getSubFolders($entry) {
  385. $children = $this->getFolderContentsById($entry['fileid']);
  386. return array_filter($children, function ($child) {
  387. return $child['mimetype'] === 'httpd/unix-directory';
  388. });
  389. }
  390. /**
  391. * Recursively remove all children of a folder
  392. *
  393. * @param array $entry the cache entry of the folder to remove the children of
  394. * @throws \OC\DatabaseException
  395. */
  396. private function removeChildren($entry) {
  397. $subFolders = $this->getSubFolders($entry);
  398. foreach ($subFolders as $folder) {
  399. $this->removeChildren($folder);
  400. }
  401. $sql = 'DELETE FROM `*PREFIX*filecache` WHERE `parent` = ?';
  402. $this->connection->executeQuery($sql, array($entry['fileid']));
  403. }
  404. /**
  405. * Move a file or folder in the cache
  406. *
  407. * @param string $source
  408. * @param string $target
  409. */
  410. public function move($source, $target) {
  411. $this->moveFromCache($this, $source, $target);
  412. }
  413. /**
  414. * Get the storage id and path needed for a move
  415. *
  416. * @param string $path
  417. * @return array [$storageId, $internalPath]
  418. */
  419. protected function getMoveInfo($path) {
  420. return [$this->getNumericStorageId(), $path];
  421. }
  422. /**
  423. * Move a file or folder in the cache
  424. *
  425. * @param \OCP\Files\Cache\ICache $sourceCache
  426. * @param string $sourcePath
  427. * @param string $targetPath
  428. * @throws \OC\DatabaseException
  429. */
  430. public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
  431. if ($sourceCache instanceof Cache) {
  432. // normalize source and target
  433. $sourcePath = $this->normalize($sourcePath);
  434. $targetPath = $this->normalize($targetPath);
  435. $sourceData = $sourceCache->get($sourcePath);
  436. $sourceId = $sourceData['fileid'];
  437. $newParentId = $this->getParentId($targetPath);
  438. list($sourceStorageId, $sourcePath) = $sourceCache->getMoveInfo($sourcePath);
  439. list($targetStorageId, $targetPath) = $this->getMoveInfo($targetPath);
  440. // sql for final update
  441. $moveSql = 'UPDATE `*PREFIX*filecache` SET `storage` = ?, `path` = ?, `path_hash` = ?, `name` = ?, `parent` =? WHERE `fileid` = ?';
  442. if ($sourceData['mimetype'] === 'httpd/unix-directory') {
  443. //find all child entries
  444. $sql = 'SELECT `path`, `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path` LIKE ?';
  445. $result = $this->connection->executeQuery($sql, [$sourceStorageId, $this->connection->escapeLikeParameter($sourcePath) . '/%']);
  446. $childEntries = $result->fetchAll();
  447. $sourceLength = strlen($sourcePath);
  448. $this->connection->beginTransaction();
  449. $query = $this->connection->prepare('UPDATE `*PREFIX*filecache` SET `storage` = ?, `path` = ?, `path_hash` = ? WHERE `fileid` = ?');
  450. foreach ($childEntries as $child) {
  451. $newTargetPath = $targetPath . substr($child['path'], $sourceLength);
  452. $query->execute([$targetStorageId, $newTargetPath, md5($newTargetPath), $child['fileid']]);
  453. }
  454. $this->connection->executeQuery($moveSql, [$targetStorageId, $targetPath, md5($targetPath), basename($targetPath), $newParentId, $sourceId]);
  455. $this->connection->commit();
  456. } else {
  457. $this->connection->executeQuery($moveSql, [$targetStorageId, $targetPath, md5($targetPath), basename($targetPath), $newParentId, $sourceId]);
  458. }
  459. } else {
  460. $this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
  461. }
  462. }
  463. /**
  464. * remove all entries for files that are stored on the storage from the cache
  465. */
  466. public function clear() {
  467. $sql = 'DELETE FROM `*PREFIX*filecache` WHERE `storage` = ?';
  468. $this->connection->executeQuery($sql, array($this->getNumericStorageId()));
  469. $sql = 'DELETE FROM `*PREFIX*storages` WHERE `id` = ?';
  470. $this->connection->executeQuery($sql, array($this->storageId));
  471. }
  472. /**
  473. * Get the scan status of a file
  474. *
  475. * - Cache::NOT_FOUND: File is not in the cache
  476. * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
  477. * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
  478. * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
  479. *
  480. * @param string $file
  481. *
  482. * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
  483. */
  484. public function getStatus($file) {
  485. // normalize file
  486. $file = $this->normalize($file);
  487. $pathHash = md5($file);
  488. $sql = 'SELECT `size` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path_hash` = ?';
  489. $result = $this->connection->executeQuery($sql, array($this->getNumericStorageId(), $pathHash));
  490. if ($row = $result->fetch()) {
  491. if ((int)$row['size'] === -1) {
  492. return self::SHALLOW;
  493. } else {
  494. return self::COMPLETE;
  495. }
  496. } else {
  497. if (isset($this->partial[$file])) {
  498. return self::PARTIAL;
  499. } else {
  500. return self::NOT_FOUND;
  501. }
  502. }
  503. }
  504. /**
  505. * search for files matching $pattern
  506. *
  507. * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
  508. * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
  509. */
  510. public function search($pattern) {
  511. // normalize pattern
  512. $pattern = $this->normalize($pattern);
  513. $sql = '
  514. SELECT `fileid`, `storage`, `path`, `parent`, `name`,
  515. `mimetype`, `mimepart`, `size`, `mtime`, `encrypted`,
  516. `etag`, `permissions`, `checksum`
  517. FROM `*PREFIX*filecache`
  518. WHERE `storage` = ? AND `name` ILIKE ?';
  519. $result = $this->connection->executeQuery($sql,
  520. [$this->getNumericStorageId(), $pattern]
  521. );
  522. $files = [];
  523. while ($row = $result->fetch()) {
  524. $row['mimetype'] = $this->mimetypeLoader->getMimetypeById($row['mimetype']);
  525. $row['mimepart'] = $this->mimetypeLoader->getMimetypeById($row['mimepart']);
  526. $files[] = $row;
  527. }
  528. return array_map(function(array $data) {
  529. return new CacheEntry($data);
  530. }, $files);
  531. }
  532. /**
  533. * search for files by mimetype
  534. *
  535. * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
  536. * where it will search for all mimetypes in the group ('image/*')
  537. * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
  538. */
  539. public function searchByMime($mimetype) {
  540. if (strpos($mimetype, '/')) {
  541. $where = '`mimetype` = ?';
  542. } else {
  543. $where = '`mimepart` = ?';
  544. }
  545. $sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`, `encrypted`, `etag`, `permissions`, `checksum`
  546. FROM `*PREFIX*filecache` WHERE ' . $where . ' AND `storage` = ?';
  547. $mimetype = $this->mimetypeLoader->getId($mimetype);
  548. $result = $this->connection->executeQuery($sql, array($mimetype, $this->getNumericStorageId()));
  549. $files = array();
  550. while ($row = $result->fetch()) {
  551. $row['mimetype'] = $this->mimetypeLoader->getMimetypeById($row['mimetype']);
  552. $row['mimepart'] = $this->mimetypeLoader->getMimetypeById($row['mimepart']);
  553. $files[] = $row;
  554. }
  555. return array_map(function (array $data) {
  556. return new CacheEntry($data);
  557. }, $files);
  558. }
  559. /**
  560. * Search for files by tag of a given users.
  561. *
  562. * Note that every user can tag files differently.
  563. *
  564. * @param string|int $tag name or tag id
  565. * @param string $userId owner of the tags
  566. * @return ICacheEntry[] file data
  567. */
  568. public function searchByTag($tag, $userId) {
  569. $sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, ' .
  570. '`mimetype`, `mimepart`, `size`, `mtime`, ' .
  571. '`encrypted`, `etag`, `permissions`, `checksum` ' .
  572. 'FROM `*PREFIX*filecache` `file`, ' .
  573. '`*PREFIX*vcategory_to_object` `tagmap`, ' .
  574. '`*PREFIX*vcategory` `tag` ' .
  575. // JOIN filecache to vcategory_to_object
  576. 'WHERE `file`.`fileid` = `tagmap`.`objid` ' .
  577. // JOIN vcategory_to_object to vcategory
  578. 'AND `tagmap`.`type` = `tag`.`type` ' .
  579. 'AND `tagmap`.`categoryid` = `tag`.`id` ' .
  580. // conditions
  581. 'AND `file`.`storage` = ? ' .
  582. 'AND `tag`.`type` = \'files\' ' .
  583. 'AND `tag`.`uid` = ? ';
  584. if (is_int($tag)) {
  585. $sql .= 'AND `tag`.`id` = ? ';
  586. } else {
  587. $sql .= 'AND `tag`.`category` = ? ';
  588. }
  589. $result = $this->connection->executeQuery(
  590. $sql,
  591. [
  592. $this->getNumericStorageId(),
  593. $userId,
  594. $tag
  595. ]
  596. );
  597. $files = array();
  598. while ($row = $result->fetch()) {
  599. $files[] = $row;
  600. }
  601. return array_map(function (array $data) {
  602. return new CacheEntry($data);
  603. }, $files);
  604. }
  605. /**
  606. * Re-calculate the folder size and the size of all parent folders
  607. *
  608. * @param string|boolean $path
  609. * @param array $data (optional) meta data of the folder
  610. */
  611. public function correctFolderSize($path, $data = null) {
  612. $this->calculateFolderSize($path, $data);
  613. if ($path !== '') {
  614. $parent = dirname($path);
  615. if ($parent === '.' or $parent === '/') {
  616. $parent = '';
  617. }
  618. $this->correctFolderSize($parent);
  619. }
  620. }
  621. /**
  622. * calculate the size of a folder and set it in the cache
  623. *
  624. * @param string $path
  625. * @param array $entry (optional) meta data of the folder
  626. * @return int
  627. */
  628. public function calculateFolderSize($path, $entry = null) {
  629. $totalSize = 0;
  630. if (is_null($entry) or !isset($entry['fileid'])) {
  631. $entry = $this->get($path);
  632. }
  633. if (isset($entry['mimetype']) && $entry['mimetype'] === 'httpd/unix-directory') {
  634. $id = $entry['fileid'];
  635. $sql = 'SELECT SUM(`size`) AS f1, MIN(`size`) AS f2 ' .
  636. 'FROM `*PREFIX*filecache` ' .
  637. 'WHERE `parent` = ? AND `storage` = ?';
  638. $result = $this->connection->executeQuery($sql, array($id, $this->getNumericStorageId()));
  639. if ($row = $result->fetch()) {
  640. $result->closeCursor();
  641. list($sum, $min) = array_values($row);
  642. $sum = 0 + $sum;
  643. $min = 0 + $min;
  644. if ($min === -1) {
  645. $totalSize = $min;
  646. } else {
  647. $totalSize = $sum;
  648. }
  649. $update = array();
  650. if ($entry['size'] !== $totalSize) {
  651. $update['size'] = $totalSize;
  652. }
  653. if (count($update) > 0) {
  654. $this->update($id, $update);
  655. }
  656. } else {
  657. $result->closeCursor();
  658. }
  659. }
  660. return $totalSize;
  661. }
  662. /**
  663. * get all file ids on the files on the storage
  664. *
  665. * @return int[]
  666. */
  667. public function getAll() {
  668. $sql = 'SELECT `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ?';
  669. $result = $this->connection->executeQuery($sql, array($this->getNumericStorageId()));
  670. $ids = array();
  671. while ($row = $result->fetch()) {
  672. $ids[] = $row['fileid'];
  673. }
  674. return $ids;
  675. }
  676. /**
  677. * find a folder in the cache which has not been fully scanned
  678. *
  679. * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
  680. * use the one with the highest id gives the best result with the background scanner, since that is most
  681. * likely the folder where we stopped scanning previously
  682. *
  683. * @return string|bool the path of the folder or false when no folder matched
  684. */
  685. public function getIncomplete() {
  686. $query = $this->connection->prepare('SELECT `path` FROM `*PREFIX*filecache`'
  687. . ' WHERE `storage` = ? AND `size` = -1 ORDER BY `fileid` DESC', 1);
  688. $query->execute([$this->getNumericStorageId()]);
  689. if ($row = $query->fetch()) {
  690. return $row['path'];
  691. } else {
  692. return false;
  693. }
  694. }
  695. /**
  696. * get the path of a file on this storage by it's file id
  697. *
  698. * @param int $id the file id of the file or folder to search
  699. * @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
  700. */
  701. public function getPathById($id) {
  702. $sql = 'SELECT `path` FROM `*PREFIX*filecache` WHERE `fileid` = ? AND `storage` = ?';
  703. $result = $this->connection->executeQuery($sql, array($id, $this->getNumericStorageId()));
  704. if ($row = $result->fetch()) {
  705. // Oracle stores empty strings as null...
  706. if ($row['path'] === null) {
  707. return '';
  708. }
  709. return $row['path'];
  710. } else {
  711. return null;
  712. }
  713. }
  714. /**
  715. * get the storage id of the storage for a file and the internal path of the file
  716. * unlike getPathById this does not limit the search to files on this storage and
  717. * instead does a global search in the cache table
  718. *
  719. * @param int $id
  720. * @deprecated use getPathById() instead
  721. * @return array first element holding the storage id, second the path
  722. */
  723. static public function getById($id) {
  724. $connection = \OC::$server->getDatabaseConnection();
  725. $sql = 'SELECT `storage`, `path` FROM `*PREFIX*filecache` WHERE `fileid` = ?';
  726. $result = $connection->executeQuery($sql, array($id));
  727. if ($row = $result->fetch()) {
  728. $numericId = $row['storage'];
  729. $path = $row['path'];
  730. } else {
  731. return null;
  732. }
  733. if ($id = Storage::getStorageId($numericId)) {
  734. return array($id, $path);
  735. } else {
  736. return null;
  737. }
  738. }
  739. /**
  740. * normalize the given path
  741. *
  742. * @param string $path
  743. * @return string
  744. */
  745. public function normalize($path) {
  746. return trim(\OC_Util::normalizeUnicode($path), '/');
  747. }
  748. }