CleanTags.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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\Repair;
  8. use OCP\DB\QueryBuilder\IQueryBuilder;
  9. use OCP\IDBConnection;
  10. use OCP\IUserManager;
  11. use OCP\Migration\IOutput;
  12. use OCP\Migration\IRepairStep;
  13. /**
  14. * Class RepairConfig
  15. *
  16. * @package OC\Repair
  17. */
  18. class CleanTags implements IRepairStep {
  19. /** @var IDBConnection */
  20. protected $connection;
  21. /** @var IUserManager */
  22. protected $userManager;
  23. protected $deletedTags = 0;
  24. /**
  25. * @param IDBConnection $connection
  26. * @param IUserManager $userManager
  27. */
  28. public function __construct(IDBConnection $connection, IUserManager $userManager) {
  29. $this->connection = $connection;
  30. $this->userManager = $userManager;
  31. }
  32. /**
  33. * @return string
  34. */
  35. public function getName() {
  36. return 'Clean tags and favorites';
  37. }
  38. /**
  39. * Updates the configuration after running an update
  40. */
  41. public function run(IOutput $output) {
  42. $this->deleteOrphanTags($output);
  43. $this->deleteOrphanFileEntries($output);
  44. $this->deleteOrphanTagEntries($output);
  45. $this->deleteOrphanCategoryEntries($output);
  46. }
  47. /**
  48. * Delete tags for deleted users
  49. */
  50. protected function deleteOrphanTags(IOutput $output) {
  51. $offset = 0;
  52. while ($this->checkTags($offset)) {
  53. $offset += 50;
  54. }
  55. $output->info(sprintf('%d tags of deleted users have been removed.', $this->deletedTags));
  56. }
  57. protected function checkTags($offset) {
  58. $query = $this->connection->getQueryBuilder();
  59. $query->select('uid')
  60. ->from('vcategory')
  61. ->groupBy('uid')
  62. ->orderBy('uid')
  63. ->setMaxResults(50)
  64. ->setFirstResult($offset);
  65. $result = $query->execute();
  66. $users = [];
  67. $hadResults = false;
  68. while ($row = $result->fetch()) {
  69. $hadResults = true;
  70. if (!$this->userManager->userExists($row['uid'])) {
  71. $users[] = $row['uid'];
  72. }
  73. }
  74. $result->closeCursor();
  75. if (!$hadResults) {
  76. // No more tags, stop looping
  77. return false;
  78. }
  79. if (!empty($users)) {
  80. $query = $this->connection->getQueryBuilder();
  81. $query->delete('vcategory')
  82. ->where($query->expr()->in('uid', $query->createNamedParameter($users, IQueryBuilder::PARAM_STR_ARRAY)));
  83. $this->deletedTags += $query->execute();
  84. }
  85. return true;
  86. }
  87. /**
  88. * Delete tag entries for deleted files
  89. */
  90. protected function deleteOrphanFileEntries(IOutput $output) {
  91. $this->deleteOrphanEntries(
  92. $output,
  93. '%d tags for delete files have been removed.',
  94. 'vcategory_to_object', 'objid',
  95. 'filecache', 'fileid', 'path_hash'
  96. );
  97. }
  98. /**
  99. * Delete tag entries for deleted tags
  100. */
  101. protected function deleteOrphanTagEntries(IOutput $output) {
  102. $this->deleteOrphanEntries(
  103. $output,
  104. '%d tag entries for deleted tags have been removed.',
  105. 'vcategory_to_object', 'categoryid',
  106. 'vcategory', 'id', 'uid'
  107. );
  108. }
  109. /**
  110. * Delete tags that have no entries
  111. */
  112. protected function deleteOrphanCategoryEntries(IOutput $output) {
  113. $this->deleteOrphanEntries(
  114. $output,
  115. '%d tags with no entries have been removed.',
  116. 'vcategory', 'id',
  117. 'vcategory_to_object', 'categoryid', 'type'
  118. );
  119. }
  120. /**
  121. * Deletes all entries from $deleteTable that do not have a matching entry in $sourceTable
  122. *
  123. * A query joins $deleteTable.$deleteId = $sourceTable.$sourceId and checks
  124. * whether $sourceNullColumn is null. If it is null, the entry in $deleteTable
  125. * is being deleted.
  126. *
  127. * @param string $repairInfo
  128. * @param string $deleteTable
  129. * @param string $deleteId
  130. * @param string $sourceTable
  131. * @param string $sourceId
  132. * @param string $sourceNullColumn If this column is null in the source table,
  133. * the entry is deleted in the $deleteTable
  134. */
  135. protected function deleteOrphanEntries(IOutput $output, $repairInfo, $deleteTable, $deleteId, $sourceTable, $sourceId, $sourceNullColumn) {
  136. $qb = $this->connection->getQueryBuilder();
  137. $qb->select('d.' . $deleteId)
  138. ->from($deleteTable, 'd')
  139. ->leftJoin('d', $sourceTable, 's', $qb->expr()->eq('d.' . $deleteId, 's.' . $sourceId))
  140. ->where(
  141. $qb->expr()->eq('d.type', $qb->expr()->literal('files'))
  142. )
  143. ->andWhere(
  144. $qb->expr()->isNull('s.' . $sourceNullColumn)
  145. );
  146. $result = $qb->execute();
  147. $orphanItems = [];
  148. while ($row = $result->fetch()) {
  149. $orphanItems[] = (int) $row[$deleteId];
  150. }
  151. if (!empty($orphanItems)) {
  152. $orphanItemsBatch = array_chunk($orphanItems, 200);
  153. foreach ($orphanItemsBatch as $items) {
  154. $qb->delete($deleteTable)
  155. ->where(
  156. $qb->expr()->eq('type', $qb->expr()->literal('files'))
  157. )
  158. ->andWhere($qb->expr()->in($deleteId, $qb->createParameter('ids')));
  159. $qb->setParameter('ids', $items, IQueryBuilder::PARAM_INT_ARRAY);
  160. $qb->execute();
  161. }
  162. }
  163. if ($repairInfo) {
  164. $output->info(sprintf($repairInfo, count($orphanItems)));
  165. }
  166. }
  167. }