Cache.php 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC\Files\Cache;
  8. use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
  9. use OC\DB\Exceptions\DbalException;
  10. use OC\DB\QueryBuilder\Sharded\ShardDefinition;
  11. use OC\Files\Search\SearchComparison;
  12. use OC\Files\Search\SearchQuery;
  13. use OC\Files\Storage\Wrapper\Encryption;
  14. use OC\SystemConfig;
  15. use OCP\DB\QueryBuilder\IQueryBuilder;
  16. use OCP\EventDispatcher\IEventDispatcher;
  17. use OCP\Files\Cache\CacheEntryInsertedEvent;
  18. use OCP\Files\Cache\CacheEntryRemovedEvent;
  19. use OCP\Files\Cache\CacheEntryUpdatedEvent;
  20. use OCP\Files\Cache\CacheInsertEvent;
  21. use OCP\Files\Cache\CacheUpdateEvent;
  22. use OCP\Files\Cache\ICache;
  23. use OCP\Files\Cache\ICacheEntry;
  24. use OCP\Files\FileInfo;
  25. use OCP\Files\IMimeTypeLoader;
  26. use OCP\Files\Search\ISearchComparison;
  27. use OCP\Files\Search\ISearchOperator;
  28. use OCP\Files\Search\ISearchQuery;
  29. use OCP\Files\Storage\IStorage;
  30. use OCP\FilesMetadata\IFilesMetadataManager;
  31. use OCP\IDBConnection;
  32. use OCP\Util;
  33. use Psr\Log\LoggerInterface;
  34. /**
  35. * Metadata cache for a storage
  36. *
  37. * The cache stores the metadata for all files and folders in a storage and is kept up to date through the following mechanisms:
  38. *
  39. * - Scanner: scans the storage and updates the cache where needed
  40. * - Watcher: checks for changes made to the filesystem outside of the Nextcloud instance and rescans files and folder when a change is detected
  41. * - Updater: listens to changes made to the filesystem inside of the Nextcloud instance and updates the cache where needed
  42. * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
  43. */
  44. class Cache implements ICache {
  45. use MoveFromCacheTrait {
  46. MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
  47. }
  48. /**
  49. * @var array partial data for the cache
  50. */
  51. protected array $partial = [];
  52. protected string $storageId;
  53. protected Storage $storageCache;
  54. protected IMimeTypeLoader $mimetypeLoader;
  55. protected IDBConnection $connection;
  56. protected SystemConfig $systemConfig;
  57. protected LoggerInterface $logger;
  58. protected QuerySearchHelper $querySearchHelper;
  59. protected IEventDispatcher $eventDispatcher;
  60. protected IFilesMetadataManager $metadataManager;
  61. public function __construct(
  62. private IStorage $storage,
  63. // this constructor is used in to many pleases to easily do proper di
  64. // so instead we group it all together
  65. ?CacheDependencies $dependencies = null,
  66. ) {
  67. $this->storageId = $storage->getId();
  68. if (strlen($this->storageId) > 64) {
  69. $this->storageId = md5($this->storageId);
  70. }
  71. if (!$dependencies) {
  72. $dependencies = \OCP\Server::get(CacheDependencies::class);
  73. }
  74. $this->storageCache = new Storage($this->storage, true, $dependencies->getConnection());
  75. $this->mimetypeLoader = $dependencies->getMimeTypeLoader();
  76. $this->connection = $dependencies->getConnection();
  77. $this->systemConfig = $dependencies->getSystemConfig();
  78. $this->logger = $dependencies->getLogger();
  79. $this->querySearchHelper = $dependencies->getQuerySearchHelper();
  80. $this->eventDispatcher = $dependencies->getEventDispatcher();
  81. $this->metadataManager = $dependencies->getMetadataManager();
  82. }
  83. protected function getQueryBuilder() {
  84. return new CacheQueryBuilder(
  85. $this->connection->getQueryBuilder(),
  86. $this->metadataManager,
  87. );
  88. }
  89. public function getStorageCache(): Storage {
  90. return $this->storageCache;
  91. }
  92. /**
  93. * Get the numeric storage id for this cache's storage
  94. *
  95. * @return int
  96. */
  97. public function getNumericStorageId() {
  98. return $this->storageCache->getNumericId();
  99. }
  100. /**
  101. * get the stored metadata of a file or folder
  102. *
  103. * @param string | int $file either the path of a file or folder or the file id for a file or folder
  104. * @return ICacheEntry|false the cache entry as array or false if the file is not found in the cache
  105. */
  106. public function get($file) {
  107. $query = $this->getQueryBuilder();
  108. $query->selectFileCache();
  109. $metadataQuery = $query->selectMetadata();
  110. if (is_string($file) || $file == '') {
  111. // normalize file
  112. $file = $this->normalize($file);
  113. $query->wherePath($file);
  114. } else { //file id
  115. $query->whereFileId($file);
  116. }
  117. $query->whereStorageId($this->getNumericStorageId());
  118. $result = $query->executeQuery();
  119. $data = $result->fetch();
  120. $result->closeCursor();
  121. //merge partial data
  122. if (!$data && is_string($file) && isset($this->partial[$file])) {
  123. return $this->partial[$file];
  124. } elseif (!$data) {
  125. return $data;
  126. } else {
  127. $data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
  128. return self::cacheEntryFromData($data, $this->mimetypeLoader);
  129. }
  130. }
  131. /**
  132. * Create a CacheEntry from database row
  133. *
  134. * @param array $data
  135. * @param IMimeTypeLoader $mimetypeLoader
  136. * @return CacheEntry
  137. */
  138. public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
  139. //fix types
  140. $data['name'] = (string)$data['name'];
  141. $data['path'] = (string)$data['path'];
  142. $data['fileid'] = (int)$data['fileid'];
  143. $data['parent'] = (int)$data['parent'];
  144. $data['size'] = Util::numericToNumber($data['size']);
  145. $data['unencrypted_size'] = Util::numericToNumber($data['unencrypted_size'] ?? 0);
  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_id'] = $data['storage'];
  151. $data['storage'] = (int)$data['storage'];
  152. $data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
  153. $data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
  154. if ($data['storage_mtime'] == 0) {
  155. $data['storage_mtime'] = $data['mtime'];
  156. }
  157. if (isset($data['f_permissions'])) {
  158. $data['scan_permissions'] = $data['f_permissions'];
  159. }
  160. $data['permissions'] = (int)$data['permissions'];
  161. if (isset($data['creation_time'])) {
  162. $data['creation_time'] = (int)$data['creation_time'];
  163. }
  164. if (isset($data['upload_time'])) {
  165. $data['upload_time'] = (int)$data['upload_time'];
  166. }
  167. return new CacheEntry($data);
  168. }
  169. /**
  170. * get the metadata of all files stored in $folder
  171. *
  172. * @param string $folder
  173. * @return ICacheEntry[]
  174. */
  175. public function getFolderContents($folder) {
  176. $fileId = $this->getId($folder);
  177. return $this->getFolderContentsById($fileId);
  178. }
  179. /**
  180. * get the metadata of all files stored in $folder
  181. *
  182. * @param int $fileId the file id of the folder
  183. * @return ICacheEntry[]
  184. */
  185. public function getFolderContentsById($fileId) {
  186. if ($fileId > -1) {
  187. $query = $this->getQueryBuilder();
  188. $query->selectFileCache()
  189. ->whereParent($fileId)
  190. ->whereStorageId($this->getNumericStorageId())
  191. ->orderBy('name', 'ASC');
  192. $metadataQuery = $query->selectMetadata();
  193. $result = $query->executeQuery();
  194. $files = $result->fetchAll();
  195. $result->closeCursor();
  196. return array_map(function (array $data) use ($metadataQuery) {
  197. $data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
  198. return self::cacheEntryFromData($data, $this->mimetypeLoader);
  199. }, $files);
  200. }
  201. return [];
  202. }
  203. /**
  204. * insert or update meta data for a file or folder
  205. *
  206. * @param string $file
  207. * @param array $data
  208. *
  209. * @return int file id
  210. * @throws \RuntimeException
  211. */
  212. public function put($file, array $data) {
  213. if (($id = $this->getId($file)) > -1) {
  214. $this->update($id, $data);
  215. return $id;
  216. } else {
  217. return $this->insert($file, $data);
  218. }
  219. }
  220. /**
  221. * insert meta data for a new file or folder
  222. *
  223. * @param string $file
  224. * @param array $data
  225. *
  226. * @return int file id
  227. * @throws \RuntimeException
  228. */
  229. public function insert($file, array $data) {
  230. // normalize file
  231. $file = $this->normalize($file);
  232. if (isset($this->partial[$file])) { //add any saved partial data
  233. $data = array_merge($this->partial[$file]->getData(), $data);
  234. unset($this->partial[$file]);
  235. }
  236. $requiredFields = ['size', 'mtime', 'mimetype'];
  237. foreach ($requiredFields as $field) {
  238. if (!isset($data[$field])) { //data not complete save as partial and return
  239. $this->partial[$file] = new CacheEntry($data);
  240. return -1;
  241. }
  242. }
  243. $data['path'] = $file;
  244. if (!isset($data['parent'])) {
  245. $data['parent'] = $this->getParentId($file);
  246. }
  247. if ($data['parent'] === -1 && $file !== '') {
  248. throw new \Exception('Parent folder not in filecache for ' . $file);
  249. }
  250. $data['name'] = basename($file);
  251. [$values, $extensionValues] = $this->normalizeData($data);
  252. $storageId = $this->getNumericStorageId();
  253. $values['storage'] = $storageId;
  254. try {
  255. $builder = $this->connection->getQueryBuilder();
  256. $builder->insert('filecache');
  257. foreach ($values as $column => $value) {
  258. $builder->setValue($column, $builder->createNamedParameter($value));
  259. }
  260. if ($builder->execute()) {
  261. $fileId = $builder->getLastInsertId();
  262. if (count($extensionValues)) {
  263. $query = $this->getQueryBuilder();
  264. $query->insert('filecache_extended');
  265. $query->hintShardKey('storage', $storageId);
  266. $query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
  267. foreach ($extensionValues as $column => $value) {
  268. $query->setValue($column, $query->createNamedParameter($value));
  269. }
  270. $query->executeStatement();
  271. }
  272. $event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
  273. $this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
  274. $this->eventDispatcher->dispatchTyped($event);
  275. return $fileId;
  276. }
  277. } catch (UniqueConstraintViolationException $e) {
  278. // entry exists already
  279. if ($this->connection->inTransaction()) {
  280. $this->connection->commit();
  281. $this->connection->beginTransaction();
  282. }
  283. }
  284. // The file was created in the mean time
  285. if (($id = $this->getId($file)) > -1) {
  286. $this->update($id, $data);
  287. return $id;
  288. } else {
  289. 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.');
  290. }
  291. }
  292. /**
  293. * update the metadata of an existing file or folder in the cache
  294. *
  295. * @param int $id the fileid of the existing file or folder
  296. * @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
  297. */
  298. public function update($id, array $data) {
  299. if (isset($data['path'])) {
  300. // normalize path
  301. $data['path'] = $this->normalize($data['path']);
  302. }
  303. if (isset($data['name'])) {
  304. // normalize path
  305. $data['name'] = $this->normalize($data['name']);
  306. }
  307. [$values, $extensionValues] = $this->normalizeData($data);
  308. if (count($values)) {
  309. $query = $this->getQueryBuilder();
  310. $query->update('filecache')
  311. ->whereFileId($id)
  312. ->whereStorageId($this->getNumericStorageId())
  313. ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
  314. return $query->expr()->orX(
  315. $query->expr()->neq($key, $query->createNamedParameter($value)),
  316. $query->expr()->isNull($key)
  317. );
  318. }, array_keys($values), array_values($values))));
  319. foreach ($values as $key => $value) {
  320. $query->set($key, $query->createNamedParameter($value));
  321. }
  322. $query->executeStatement();
  323. }
  324. if (count($extensionValues)) {
  325. try {
  326. $query = $this->getQueryBuilder();
  327. $query->insert('filecache_extended');
  328. $query->hintShardKey('storage', $this->getNumericStorageId());
  329. $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
  330. foreach ($extensionValues as $column => $value) {
  331. $query->setValue($column, $query->createNamedParameter($value));
  332. }
  333. $query->execute();
  334. } catch (UniqueConstraintViolationException $e) {
  335. $query = $this->getQueryBuilder();
  336. $query->update('filecache_extended')
  337. ->whereFileId($id)
  338. ->hintShardKey('storage', $this->getNumericStorageId())
  339. ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
  340. return $query->expr()->orX(
  341. $query->expr()->neq($key, $query->createNamedParameter($value)),
  342. $query->expr()->isNull($key)
  343. );
  344. }, array_keys($extensionValues), array_values($extensionValues))));
  345. foreach ($extensionValues as $key => $value) {
  346. $query->set($key, $query->createNamedParameter($value));
  347. }
  348. $query->executeStatement();
  349. }
  350. }
  351. $path = $this->getPathById($id);
  352. // path can still be null if the file doesn't exist
  353. if ($path !== null) {
  354. $event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
  355. $this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
  356. $this->eventDispatcher->dispatchTyped($event);
  357. }
  358. }
  359. /**
  360. * extract query parts and params array from data array
  361. *
  362. * @param array $data
  363. * @return array
  364. */
  365. protected function normalizeData(array $data): array {
  366. $fields = [
  367. 'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
  368. 'etag', 'permissions', 'checksum', 'storage', 'unencrypted_size'];
  369. $extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
  370. $doNotCopyStorageMTime = false;
  371. if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
  372. // this horrific magic tells it to not copy storage_mtime to mtime
  373. unset($data['mtime']);
  374. $doNotCopyStorageMTime = true;
  375. }
  376. $params = [];
  377. $extensionParams = [];
  378. foreach ($data as $name => $value) {
  379. if (in_array($name, $fields)) {
  380. if ($name === 'path') {
  381. $params['path_hash'] = md5($value);
  382. } elseif ($name === 'mimetype') {
  383. $params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
  384. $value = $this->mimetypeLoader->getId($value);
  385. } elseif ($name === 'storage_mtime') {
  386. if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
  387. $params['mtime'] = $value;
  388. }
  389. } elseif ($name === 'encrypted') {
  390. if (isset($data['encryptedVersion'])) {
  391. $value = $data['encryptedVersion'];
  392. } else {
  393. // Boolean to integer conversion
  394. $value = $value ? 1 : 0;
  395. }
  396. }
  397. $params[$name] = $value;
  398. }
  399. if (in_array($name, $extensionFields)) {
  400. $extensionParams[$name] = $value;
  401. }
  402. }
  403. return [$params, array_filter($extensionParams)];
  404. }
  405. /**
  406. * get the file id for a file
  407. *
  408. * 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
  409. *
  410. * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
  411. *
  412. * @param string $file
  413. * @return int
  414. */
  415. public function getId($file) {
  416. // normalize file
  417. $file = $this->normalize($file);
  418. $query = $this->getQueryBuilder();
  419. $query->select('fileid')
  420. ->from('filecache')
  421. ->whereStorageId($this->getNumericStorageId())
  422. ->wherePath($file);
  423. $result = $query->executeQuery();
  424. $id = $result->fetchOne();
  425. $result->closeCursor();
  426. return $id === false ? -1 : (int)$id;
  427. }
  428. /**
  429. * get the id of the parent folder of a file
  430. *
  431. * @param string $file
  432. * @return int
  433. */
  434. public function getParentId($file) {
  435. if ($file === '') {
  436. return -1;
  437. } else {
  438. $parent = $this->getParentPath($file);
  439. return (int)$this->getId($parent);
  440. }
  441. }
  442. private function getParentPath($path) {
  443. $parent = dirname($path);
  444. if ($parent === '.') {
  445. $parent = '';
  446. }
  447. return $parent;
  448. }
  449. /**
  450. * check if a file is available in the cache
  451. *
  452. * @param string $file
  453. * @return bool
  454. */
  455. public function inCache($file) {
  456. return $this->getId($file) != -1;
  457. }
  458. /**
  459. * remove a file or folder from the cache
  460. *
  461. * when removing a folder from the cache all files and folders inside the folder will be removed as well
  462. *
  463. * @param string $file
  464. */
  465. public function remove($file) {
  466. $entry = $this->get($file);
  467. if ($entry instanceof ICacheEntry) {
  468. $query = $this->getQueryBuilder();
  469. $query->delete('filecache')
  470. ->whereStorageId($this->getNumericStorageId())
  471. ->whereFileId($entry->getId());
  472. $query->executeStatement();
  473. $query = $this->getQueryBuilder();
  474. $query->delete('filecache_extended')
  475. ->whereFileId($entry->getId())
  476. ->hintShardKey('storage', $this->getNumericStorageId());
  477. $query->executeStatement();
  478. if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
  479. $this->removeChildren($entry);
  480. }
  481. $this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
  482. }
  483. }
  484. /**
  485. * Remove all children of a folder
  486. *
  487. * @param ICacheEntry $entry the cache entry of the folder to remove the children of
  488. * @throws \OC\DatabaseException
  489. */
  490. private function removeChildren(ICacheEntry $entry) {
  491. $parentIds = [$entry->getId()];
  492. $queue = [$entry->getId()];
  493. $deletedIds = [];
  494. $deletedPaths = [];
  495. // we walk depth first through the file tree, removing all filecache_extended attributes while we walk
  496. // and collecting all folder ids to later use to delete the filecache entries
  497. while ($entryId = array_pop($queue)) {
  498. $children = $this->getFolderContentsById($entryId);
  499. $childIds = array_map(function (ICacheEntry $cacheEntry) {
  500. return $cacheEntry->getId();
  501. }, $children);
  502. $childPaths = array_map(function (ICacheEntry $cacheEntry) {
  503. return $cacheEntry->getPath();
  504. }, $children);
  505. foreach ($childIds as $childId) {
  506. $deletedIds[] = $childId;
  507. }
  508. foreach ($childPaths as $childPath) {
  509. $deletedPaths[] = $childPath;
  510. }
  511. $query = $this->getQueryBuilder();
  512. $query->delete('filecache_extended')
  513. ->where($query->expr()->in('fileid', $query->createParameter('childIds')))
  514. ->hintShardKey('storage', $this->getNumericStorageId());
  515. foreach (array_chunk($childIds, 1000) as $childIdChunk) {
  516. $query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
  517. $query->executeStatement();
  518. }
  519. /** @var ICacheEntry[] $childFolders */
  520. $childFolders = [];
  521. foreach ($children as $child) {
  522. if ($child->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
  523. $childFolders[] = $child;
  524. }
  525. }
  526. foreach ($childFolders as $folder) {
  527. $parentIds[] = $folder->getId();
  528. $queue[] = $folder->getId();
  529. }
  530. }
  531. $query = $this->getQueryBuilder();
  532. $query->delete('filecache')
  533. ->whereStorageId($this->getNumericStorageId())
  534. ->whereParentInParameter('parentIds');
  535. // Sorting before chunking allows the db to find the entries close to each
  536. // other in the index
  537. sort($parentIds, SORT_NUMERIC);
  538. foreach (array_chunk($parentIds, 1000) as $parentIdChunk) {
  539. $query->setParameter('parentIds', $parentIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
  540. $query->executeStatement();
  541. }
  542. foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) {
  543. $cacheEntryRemovedEvent = new CacheEntryRemovedEvent(
  544. $this->storage,
  545. $filePath,
  546. $fileId,
  547. $this->getNumericStorageId()
  548. );
  549. $this->eventDispatcher->dispatchTyped($cacheEntryRemovedEvent);
  550. }
  551. }
  552. /**
  553. * Move a file or folder in the cache
  554. *
  555. * @param string $source
  556. * @param string $target
  557. */
  558. public function move($source, $target) {
  559. $this->moveFromCache($this, $source, $target);
  560. }
  561. /**
  562. * Get the storage id and path needed for a move
  563. *
  564. * @param string $path
  565. * @return array [$storageId, $internalPath]
  566. */
  567. protected function getMoveInfo($path) {
  568. return [$this->getNumericStorageId(), $path];
  569. }
  570. protected function hasEncryptionWrapper(): bool {
  571. return $this->storage->instanceOfStorage(Encryption::class);
  572. }
  573. /**
  574. * Move a file or folder in the cache
  575. *
  576. * @param ICache $sourceCache
  577. * @param string $sourcePath
  578. * @param string $targetPath
  579. * @throws \OC\DatabaseException
  580. * @throws \Exception if the given storages have an invalid id
  581. */
  582. public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
  583. if ($sourceCache instanceof Cache) {
  584. // normalize source and target
  585. $sourcePath = $this->normalize($sourcePath);
  586. $targetPath = $this->normalize($targetPath);
  587. $sourceData = $sourceCache->get($sourcePath);
  588. if (!$sourceData) {
  589. throw new \Exception('Invalid source storage path: ' . $sourcePath);
  590. }
  591. $shardDefinition = $this->connection->getShardDefinition('filecache');
  592. if (
  593. $shardDefinition &&
  594. $shardDefinition->getShardForKey($sourceCache->getNumericStorageId()) !== $shardDefinition->getShardForKey($this->getNumericStorageId())
  595. ) {
  596. $this->moveFromStorageSharded($shardDefinition, $sourceCache, $sourceData, $targetPath);
  597. return;
  598. }
  599. $sourceId = $sourceData['fileid'];
  600. $newParentId = $this->getParentId($targetPath);
  601. [$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
  602. [$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
  603. if (is_null($sourceStorageId) || $sourceStorageId === false) {
  604. throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
  605. }
  606. if (is_null($targetStorageId) || $targetStorageId === false) {
  607. throw new \Exception('Invalid target storage id: ' . $targetStorageId);
  608. }
  609. if ($sourceData['mimetype'] === 'httpd/unix-directory') {
  610. //update all child entries
  611. $sourceLength = mb_strlen($sourcePath);
  612. $childIds = $this->getChildIds($sourceStorageId, $sourcePath);
  613. $childChunks = array_chunk($childIds, 1000);
  614. $query = $this->getQueryBuilder();
  615. $fun = $query->func();
  616. $newPathFunction = $fun->concat(
  617. $query->createNamedParameter($targetPath),
  618. $fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
  619. );
  620. $query->update('filecache')
  621. ->set('path_hash', $fun->md5($newPathFunction))
  622. ->set('path', $newPathFunction)
  623. ->whereStorageId($sourceStorageId)
  624. ->andWhere($query->expr()->in('fileid', $query->createParameter('files')));
  625. if ($sourceStorageId !== $targetStorageId) {
  626. $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
  627. }
  628. // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
  629. if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
  630. $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
  631. }
  632. // Retry transaction in case of RetryableException like deadlocks.
  633. // Retry up to 4 times because we should receive up to 4 concurrent requests from the frontend
  634. $retryLimit = 4;
  635. for ($i = 1; $i <= $retryLimit; $i++) {
  636. try {
  637. $this->connection->beginTransaction();
  638. foreach ($childChunks as $chunk) {
  639. $query->setParameter('files', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
  640. $query->executeStatement();
  641. }
  642. break;
  643. } catch (\OC\DatabaseException $e) {
  644. $this->connection->rollBack();
  645. throw $e;
  646. } catch (DbalException $e) {
  647. $this->connection->rollBack();
  648. if (!$e->isRetryable()) {
  649. throw $e;
  650. }
  651. // Simply throw if we already retried 4 times.
  652. if ($i === $retryLimit) {
  653. throw $e;
  654. }
  655. // Sleep a bit to give some time to the other transaction to finish.
  656. usleep(100 * 1000 * $i);
  657. }
  658. }
  659. } else {
  660. $this->connection->beginTransaction();
  661. }
  662. $query = $this->getQueryBuilder();
  663. $query->update('filecache')
  664. ->set('path', $query->createNamedParameter($targetPath))
  665. ->set('path_hash', $query->createNamedParameter(md5($targetPath)))
  666. ->set('name', $query->createNamedParameter(basename($targetPath)))
  667. ->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
  668. ->whereStorageId($sourceStorageId)
  669. ->whereFileId($sourceId);
  670. if ($sourceStorageId !== $targetStorageId) {
  671. $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
  672. }
  673. // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
  674. if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
  675. $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
  676. }
  677. $query->executeStatement();
  678. $this->connection->commit();
  679. if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
  680. $this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
  681. $event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
  682. $this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
  683. $this->eventDispatcher->dispatchTyped($event);
  684. } else {
  685. $event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
  686. $this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
  687. $this->eventDispatcher->dispatchTyped($event);
  688. }
  689. } else {
  690. $this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
  691. }
  692. }
  693. private function getChildIds(int $storageId, string $path): array {
  694. $query = $this->connection->getQueryBuilder();
  695. $query->select('fileid')
  696. ->from('filecache')
  697. ->where($query->expr()->eq('storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
  698. ->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($path) . '/%')));
  699. return $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
  700. }
  701. /**
  702. * remove all entries for files that are stored on the storage from the cache
  703. */
  704. public function clear() {
  705. $query = $this->getQueryBuilder();
  706. $query->delete('filecache')
  707. ->whereStorageId($this->getNumericStorageId());
  708. $query->executeStatement();
  709. $query = $this->connection->getQueryBuilder();
  710. $query->delete('storages')
  711. ->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
  712. $query->executeStatement();
  713. }
  714. /**
  715. * Get the scan status of a file
  716. *
  717. * - Cache::NOT_FOUND: File is not in the cache
  718. * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
  719. * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
  720. * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
  721. *
  722. * @param string $file
  723. *
  724. * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
  725. */
  726. public function getStatus($file) {
  727. // normalize file
  728. $file = $this->normalize($file);
  729. $query = $this->getQueryBuilder();
  730. $query->select('size')
  731. ->from('filecache')
  732. ->whereStorageId($this->getNumericStorageId())
  733. ->wherePath($file);
  734. $result = $query->executeQuery();
  735. $size = $result->fetchOne();
  736. $result->closeCursor();
  737. if ($size !== false) {
  738. if ((int)$size === -1) {
  739. return self::SHALLOW;
  740. } else {
  741. return self::COMPLETE;
  742. }
  743. } else {
  744. if (isset($this->partial[$file])) {
  745. return self::PARTIAL;
  746. } else {
  747. return self::NOT_FOUND;
  748. }
  749. }
  750. }
  751. /**
  752. * search for files matching $pattern
  753. *
  754. * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
  755. * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
  756. */
  757. public function search($pattern) {
  758. $operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', $pattern);
  759. return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
  760. }
  761. /**
  762. * search for files by mimetype
  763. *
  764. * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
  765. * where it will search for all mimetypes in the group ('image/*')
  766. * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
  767. */
  768. public function searchByMime($mimetype) {
  769. if (!str_contains($mimetype, '/')) {
  770. $operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
  771. } else {
  772. $operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
  773. }
  774. return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
  775. }
  776. public function searchQuery(ISearchQuery $query) {
  777. return current($this->querySearchHelper->searchInCaches($query, [$this]));
  778. }
  779. /**
  780. * Re-calculate the folder size and the size of all parent folders
  781. *
  782. * @param string|boolean $path
  783. * @param array $data (optional) meta data of the folder
  784. */
  785. public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
  786. $this->calculateFolderSize($path, $data);
  787. if ($path !== '') {
  788. $parent = dirname($path);
  789. if ($parent === '.' || $parent === '/') {
  790. $parent = '';
  791. }
  792. if ($isBackgroundScan) {
  793. $parentData = $this->get($parent);
  794. if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
  795. $this->correctFolderSize($parent, $parentData, $isBackgroundScan);
  796. }
  797. } else {
  798. $this->correctFolderSize($parent);
  799. }
  800. }
  801. }
  802. /**
  803. * get the incomplete count that shares parent $folder
  804. *
  805. * @param int $fileId the file id of the folder
  806. * @return int
  807. */
  808. public function getIncompleteChildrenCount($fileId) {
  809. if ($fileId > -1) {
  810. $query = $this->getQueryBuilder();
  811. $query->select($query->func()->count())
  812. ->from('filecache')
  813. ->whereParent($fileId)
  814. ->whereStorageId($this->getNumericStorageId())
  815. ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
  816. $result = $query->executeQuery();
  817. $size = (int)$result->fetchOne();
  818. $result->closeCursor();
  819. return $size;
  820. }
  821. return -1;
  822. }
  823. /**
  824. * calculate the size of a folder and set it in the cache
  825. *
  826. * @param string $path
  827. * @param array|null|ICacheEntry $entry (optional) meta data of the folder
  828. * @return int|float
  829. */
  830. public function calculateFolderSize($path, $entry = null) {
  831. return $this->calculateFolderSizeInner($path, $entry);
  832. }
  833. /**
  834. * inner function because we can't add new params to the public function without breaking any child classes
  835. *
  836. * @param string $path
  837. * @param array|null|ICacheEntry $entry (optional) meta data of the folder
  838. * @param bool $ignoreUnknown don't mark the folder size as unknown if any of it's children are unknown
  839. * @return int|float
  840. */
  841. protected function calculateFolderSizeInner(string $path, $entry = null, bool $ignoreUnknown = false) {
  842. $totalSize = 0;
  843. if (is_null($entry) || !isset($entry['fileid'])) {
  844. $entry = $this->get($path);
  845. }
  846. if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
  847. $id = $entry['fileid'];
  848. $query = $this->getQueryBuilder();
  849. $query->select('size', 'unencrypted_size')
  850. ->from('filecache')
  851. ->whereStorageId($this->getNumericStorageId())
  852. ->whereParent($id);
  853. if ($ignoreUnknown) {
  854. $query->andWhere($query->expr()->gte('size', $query->createNamedParameter(0)));
  855. }
  856. $result = $query->executeQuery();
  857. $rows = $result->fetchAll();
  858. $result->closeCursor();
  859. if ($rows) {
  860. $sizes = array_map(function (array $row) {
  861. return Util::numericToNumber($row['size']);
  862. }, $rows);
  863. $unencryptedOnlySizes = array_map(function (array $row) {
  864. return Util::numericToNumber($row['unencrypted_size']);
  865. }, $rows);
  866. $unencryptedSizes = array_map(function (array $row) {
  867. return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
  868. }, $rows);
  869. $sum = array_sum($sizes);
  870. $min = min($sizes);
  871. $unencryptedSum = array_sum($unencryptedSizes);
  872. $unencryptedMin = min($unencryptedSizes);
  873. $unencryptedMax = max($unencryptedOnlySizes);
  874. $sum = 0 + $sum;
  875. $min = 0 + $min;
  876. if ($min === -1) {
  877. $totalSize = $min;
  878. } else {
  879. $totalSize = $sum;
  880. }
  881. if ($unencryptedMin === -1 || $min === -1) {
  882. $unencryptedTotal = $unencryptedMin;
  883. } else {
  884. $unencryptedTotal = $unencryptedSum;
  885. }
  886. } else {
  887. $totalSize = 0;
  888. $unencryptedTotal = 0;
  889. $unencryptedMax = 0;
  890. }
  891. // only set unencrypted size for a folder if any child entries have it set, or the folder is empty
  892. $shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || $entry['unencrypted_size'] > 0;
  893. if ($entry['size'] !== $totalSize || ($entry['unencrypted_size'] !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) {
  894. if ($shouldWriteUnEncryptedSize) {
  895. // if all children have an unencrypted size of 0, just set the folder unencrypted size to 0 instead of summing the sizes
  896. if ($unencryptedMax === 0) {
  897. $unencryptedTotal = 0;
  898. }
  899. $this->update($id, [
  900. 'size' => $totalSize,
  901. 'unencrypted_size' => $unencryptedTotal,
  902. ]);
  903. } else {
  904. $this->update($id, [
  905. 'size' => $totalSize,
  906. ]);
  907. }
  908. }
  909. }
  910. return $totalSize;
  911. }
  912. /**
  913. * get all file ids on the files on the storage
  914. *
  915. * @return int[]
  916. */
  917. public function getAll() {
  918. $query = $this->getQueryBuilder();
  919. $query->select('fileid')
  920. ->from('filecache')
  921. ->whereStorageId($this->getNumericStorageId());
  922. $result = $query->executeQuery();
  923. $files = $result->fetchAll(\PDO::FETCH_COLUMN);
  924. $result->closeCursor();
  925. return array_map(function ($id) {
  926. return (int)$id;
  927. }, $files);
  928. }
  929. /**
  930. * find a folder in the cache which has not been fully scanned
  931. *
  932. * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
  933. * use the one with the highest id gives the best result with the background scanner, since that is most
  934. * likely the folder where we stopped scanning previously
  935. *
  936. * @return string|false the path of the folder or false when no folder matched
  937. */
  938. public function getIncomplete() {
  939. // we select the fileid here first instead of directly selecting the path since this helps mariadb/mysql
  940. // to use the correct index.
  941. // The overhead of this should be minimal since the cost of selecting the path by id should be much lower
  942. // than the cost of finding an item with size < 0
  943. $query = $this->getQueryBuilder();
  944. $query->select('fileid')
  945. ->from('filecache')
  946. ->whereStorageId($this->getNumericStorageId())
  947. ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
  948. ->orderBy('fileid', 'DESC')
  949. ->setMaxResults(1);
  950. $result = $query->executeQuery();
  951. $id = $result->fetchOne();
  952. $result->closeCursor();
  953. if ($id === false) {
  954. return false;
  955. }
  956. $path = $this->getPathById($id);
  957. return $path ?? false;
  958. }
  959. /**
  960. * get the path of a file on this storage by it's file id
  961. *
  962. * @param int $id the file id of the file or folder to search
  963. * @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
  964. */
  965. public function getPathById($id) {
  966. $query = $this->getQueryBuilder();
  967. $query->select('path')
  968. ->from('filecache')
  969. ->whereStorageId($this->getNumericStorageId())
  970. ->whereFileId($id);
  971. $result = $query->executeQuery();
  972. $path = $result->fetchOne();
  973. $result->closeCursor();
  974. if ($path === false) {
  975. return null;
  976. }
  977. return (string)$path;
  978. }
  979. /**
  980. * get the storage id of the storage for a file and the internal path of the file
  981. * unlike getPathById this does not limit the search to files on this storage and
  982. * instead does a global search in the cache table
  983. *
  984. * @param int $id
  985. * @return array first element holding the storage id, second the path
  986. * @deprecated 17.0.0 use getPathById() instead
  987. */
  988. public static function getById($id) {
  989. $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
  990. $query->select('path', 'storage')
  991. ->from('filecache')
  992. ->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
  993. $result = $query->executeQuery();
  994. $row = $result->fetch();
  995. $result->closeCursor();
  996. if ($row) {
  997. $numericId = $row['storage'];
  998. $path = $row['path'];
  999. } else {
  1000. return null;
  1001. }
  1002. if ($id = Storage::getStorageId($numericId)) {
  1003. return [$id, $path];
  1004. } else {
  1005. return null;
  1006. }
  1007. }
  1008. /**
  1009. * normalize the given path
  1010. *
  1011. * @param string $path
  1012. * @return string
  1013. */
  1014. public function normalize($path) {
  1015. return trim(\OC_Util::normalizeUnicode($path), '/');
  1016. }
  1017. /**
  1018. * Copy a file or folder in the cache
  1019. *
  1020. * @param ICache $sourceCache
  1021. * @param ICacheEntry $sourceEntry
  1022. * @param string $targetPath
  1023. * @return int fileId of copied entry
  1024. */
  1025. public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
  1026. if ($sourceEntry->getId() < 0) {
  1027. throw new \RuntimeException('Invalid source cache entry on copyFromCache');
  1028. }
  1029. $data = $this->cacheEntryToArray($sourceEntry);
  1030. // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
  1031. if ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
  1032. $data['encrypted'] = 0;
  1033. }
  1034. $fileId = $this->put($targetPath, $data);
  1035. if ($fileId <= 0) {
  1036. throw new \RuntimeException('Failed to copy to ' . $targetPath . ' from cache with source data ' . json_encode($data) . ' ');
  1037. }
  1038. if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
  1039. $folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
  1040. foreach ($folderContent as $subEntry) {
  1041. $subTargetPath = $targetPath . '/' . $subEntry->getName();
  1042. $this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
  1043. }
  1044. }
  1045. return $fileId;
  1046. }
  1047. private function cacheEntryToArray(ICacheEntry $entry): array {
  1048. $data = [
  1049. 'size' => $entry->getSize(),
  1050. 'mtime' => $entry->getMTime(),
  1051. 'storage_mtime' => $entry->getStorageMTime(),
  1052. 'mimetype' => $entry->getMimeType(),
  1053. 'mimepart' => $entry->getMimePart(),
  1054. 'etag' => $entry->getEtag(),
  1055. 'permissions' => $entry->getPermissions(),
  1056. 'encrypted' => $entry->isEncrypted(),
  1057. 'creation_time' => $entry->getCreationTime(),
  1058. 'upload_time' => $entry->getUploadTime(),
  1059. 'metadata_etag' => $entry->getMetadataEtag(),
  1060. ];
  1061. if ($entry instanceof CacheEntry && isset($entry['scan_permissions'])) {
  1062. $data['permissions'] = $entry['scan_permissions'];
  1063. }
  1064. return $data;
  1065. }
  1066. public function getQueryFilterForStorage(): ISearchOperator {
  1067. return new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $this->getNumericStorageId());
  1068. }
  1069. public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
  1070. if ($rawEntry->getStorageId() === $this->getNumericStorageId()) {
  1071. return $rawEntry;
  1072. } else {
  1073. return null;
  1074. }
  1075. }
  1076. private function moveFromStorageSharded(ShardDefinition $shardDefinition, ICache $sourceCache, ICacheEntry $sourceEntry, $targetPath): void {
  1077. if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
  1078. $fileIds = $this->getChildIds($sourceCache->getNumericStorageId(), $sourceEntry->getPath());
  1079. } else {
  1080. $fileIds = [];
  1081. }
  1082. $fileIds[] = $sourceEntry->getId();
  1083. $helper = $this->connection->getCrossShardMoveHelper();
  1084. $sourceConnection = $helper->getConnection($shardDefinition, $sourceCache->getNumericStorageId());
  1085. $targetConnection = $helper->getConnection($shardDefinition, $this->getNumericStorageId());
  1086. $cacheItems = $helper->loadItems($sourceConnection, 'filecache', 'fileid', $fileIds);
  1087. $extendedItems = $helper->loadItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
  1088. $metadataItems = $helper->loadItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
  1089. // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
  1090. $removeEncryptedFlag = ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper()) && !$this->hasEncryptionWrapper();
  1091. $sourcePathLength = strlen($sourceEntry->getPath());
  1092. foreach ($cacheItems as &$cacheItem) {
  1093. if ($cacheItem['path'] === $sourceEntry->getPath()) {
  1094. $cacheItem['path'] = $targetPath;
  1095. $cacheItem['parent'] = $this->getParentId($targetPath);
  1096. $cacheItem['name'] = basename($cacheItem['path']);
  1097. } else {
  1098. $cacheItem['path'] = $targetPath . '/' . substr($cacheItem['path'], $sourcePathLength + 1); // +1 for the leading slash
  1099. }
  1100. $cacheItem['path_hash'] = md5($cacheItem['path']);
  1101. $cacheItem['storage'] = $this->getNumericStorageId();
  1102. if ($removeEncryptedFlag) {
  1103. $cacheItem['encrypted'] = 0;
  1104. }
  1105. }
  1106. $targetConnection->beginTransaction();
  1107. try {
  1108. $helper->saveItems($targetConnection, 'filecache', $cacheItems);
  1109. $helper->saveItems($targetConnection, 'filecache_extended', $extendedItems);
  1110. $helper->saveItems($targetConnection, 'files_metadata', $metadataItems);
  1111. } catch (\Exception $e) {
  1112. $targetConnection->rollback();
  1113. throw $e;
  1114. }
  1115. $sourceConnection->beginTransaction();
  1116. try {
  1117. $helper->deleteItems($sourceConnection, 'filecache', 'fileid', $fileIds);
  1118. $helper->deleteItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
  1119. $helper->deleteItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
  1120. } catch (\Exception $e) {
  1121. $targetConnection->rollback();
  1122. $sourceConnection->rollBack();
  1123. throw $e;
  1124. }
  1125. try {
  1126. $sourceConnection->commit();
  1127. } catch (\Exception $e) {
  1128. $targetConnection->rollback();
  1129. throw $e;
  1130. }
  1131. $targetConnection->commit();
  1132. }
  1133. }