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