MetadataRequestService.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\FilesMetadata\Service;
  8. use OC\FilesMetadata\Model\FilesMetadata;
  9. use OCP\DB\Exception;
  10. use OCP\DB\QueryBuilder\IQueryBuilder;
  11. use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
  12. use OCP\FilesMetadata\Model\IFilesMetadata;
  13. use OCP\IDBConnection;
  14. use Psr\Log\LoggerInterface;
  15. /**
  16. * manage sql request to the metadata table
  17. */
  18. class MetadataRequestService {
  19. public const TABLE_METADATA = 'files_metadata';
  20. public function __construct(
  21. private IDBConnection $dbConnection,
  22. private LoggerInterface $logger,
  23. ) {
  24. }
  25. private function getStorageId(IFilesMetadata $filesMetadata): int {
  26. if ($filesMetadata instanceof FilesMetadata) {
  27. $storage = $filesMetadata->getStorageId();
  28. if ($storage) {
  29. return $storage;
  30. }
  31. }
  32. // all code paths that lead to saving metadata *should* have the storage id set
  33. // this fallback is there just in case
  34. $query = $this->dbConnection->getQueryBuilder();
  35. $query->select('storage')
  36. ->from('filecache')
  37. ->where($query->expr()->eq('fileid', $query->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT)));
  38. $storageId = $query->executeQuery()->fetchColumn();
  39. if ($filesMetadata instanceof FilesMetadata) {
  40. $filesMetadata->setStorageId($storageId);
  41. }
  42. return $storageId;
  43. }
  44. /**
  45. * store metadata into database
  46. *
  47. * @param IFilesMetadata $filesMetadata
  48. *
  49. * @throws Exception
  50. */
  51. public function store(IFilesMetadata $filesMetadata): void {
  52. $qb = $this->dbConnection->getQueryBuilder();
  53. $qb->insert(self::TABLE_METADATA)
  54. ->hintShardKey('storage', $this->getStorageId($filesMetadata))
  55. ->setValue('file_id', $qb->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT))
  56. ->setValue('json', $qb->createNamedParameter(json_encode($filesMetadata->jsonSerialize())))
  57. ->setValue('sync_token', $qb->createNamedParameter($this->generateSyncToken()))
  58. ->setValue('last_update', (string)$qb->createFunction('NOW()'));
  59. $qb->executeStatement();
  60. }
  61. /**
  62. * returns metadata for a file id
  63. *
  64. * @param int $fileId file id
  65. *
  66. * @return IFilesMetadata
  67. * @throws FilesMetadataNotFoundException if no metadata are found in database
  68. */
  69. public function getMetadataFromFileId(int $fileId): IFilesMetadata {
  70. try {
  71. $qb = $this->dbConnection->getQueryBuilder();
  72. $qb->select('json', 'sync_token')->from(self::TABLE_METADATA);
  73. $qb->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
  74. $result = $qb->executeQuery();
  75. $data = $result->fetch();
  76. $result->closeCursor();
  77. } catch (Exception $e) {
  78. $this->logger->warning('exception while getMetadataFromDatabase()', ['exception' => $e, 'fileId' => $fileId]);
  79. throw new FilesMetadataNotFoundException();
  80. }
  81. if ($data === false) {
  82. throw new FilesMetadataNotFoundException();
  83. }
  84. $metadata = new FilesMetadata($fileId);
  85. $metadata->importFromDatabase($data);
  86. return $metadata;
  87. }
  88. /**
  89. * returns metadata for multiple file ids
  90. *
  91. * @param array $fileIds file ids
  92. *
  93. * @return array File ID is the array key, files without metadata are not returned in the array
  94. * @psalm-return array<int, IFilesMetadata>
  95. */
  96. public function getMetadataFromFileIds(array $fileIds): array {
  97. $qb = $this->dbConnection->getQueryBuilder();
  98. $qb->select('file_id', 'json', 'sync_token')->from(self::TABLE_METADATA);
  99. $qb->where($qb->expr()->in('file_id', $qb->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)));
  100. $list = [];
  101. $result = $qb->executeQuery();
  102. while ($data = $result->fetch()) {
  103. $fileId = (int)$data['file_id'];
  104. $metadata = new FilesMetadata($fileId);
  105. try {
  106. $metadata->importFromDatabase($data);
  107. } catch (FilesMetadataNotFoundException) {
  108. continue;
  109. }
  110. $list[$fileId] = $metadata;
  111. }
  112. $result->closeCursor();
  113. return $list;
  114. }
  115. /**
  116. * drop metadata related to a file id
  117. *
  118. * @param int $fileId file id
  119. *
  120. * @return void
  121. * @throws Exception
  122. */
  123. public function dropMetadata(int $fileId): void {
  124. $qb = $this->dbConnection->getQueryBuilder();
  125. $qb->delete(self::TABLE_METADATA)
  126. ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
  127. $qb->executeStatement();
  128. }
  129. /**
  130. * update metadata in the database
  131. *
  132. * @param IFilesMetadata $filesMetadata metadata
  133. *
  134. * @return int number of affected rows
  135. * @throws Exception
  136. */
  137. public function updateMetadata(IFilesMetadata $filesMetadata): int {
  138. $qb = $this->dbConnection->getQueryBuilder();
  139. $expr = $qb->expr();
  140. $qb->update(self::TABLE_METADATA)
  141. ->hintShardKey('files_metadata', $this->getStorageId($filesMetadata))
  142. ->set('json', $qb->createNamedParameter(json_encode($filesMetadata->jsonSerialize())))
  143. ->set('sync_token', $qb->createNamedParameter($this->generateSyncToken()))
  144. ->set('last_update', $qb->createFunction('NOW()'))
  145. ->where(
  146. $expr->andX(
  147. $expr->eq('file_id', $qb->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT)),
  148. $expr->eq('sync_token', $qb->createNamedParameter($filesMetadata->getSyncToken()))
  149. )
  150. );
  151. return $qb->executeStatement();
  152. }
  153. /**
  154. * generate a random token
  155. * @return string
  156. */
  157. private function generateSyncToken(): string {
  158. $chars = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890';
  159. $str = '';
  160. $max = strlen($chars);
  161. for ($i = 0; $i < 7; $i++) {
  162. try {
  163. $str .= $chars[random_int(0, $max - 2)];
  164. } catch (\Exception $e) {
  165. $this->logger->warning('exception during generateSyncToken', ['exception' => $e]);
  166. }
  167. }
  168. return $str;
  169. }
  170. }