CleanupRemoteStorages.php 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud GmbH.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\Files_Sharing\Command;
  8. use OCP\DB\QueryBuilder\IQueryBuilder;
  9. use OCP\Federation\ICloudIdManager;
  10. use OCP\IDBConnection;
  11. use Symfony\Component\Console\Command\Command;
  12. use Symfony\Component\Console\Input\InputInterface;
  13. use Symfony\Component\Console\Input\InputOption;
  14. use Symfony\Component\Console\Output\OutputInterface;
  15. /**
  16. * Cleanup 'shared::' storage entries that have no matching entries in the
  17. * shares_external table.
  18. */
  19. class CleanupRemoteStorages extends Command {
  20. /**
  21. * @var IDBConnection
  22. */
  23. protected $connection;
  24. /**
  25. * @var ICloudIdManager
  26. */
  27. private $cloudIdManager;
  28. public function __construct(IDBConnection $connection, ICloudIdManager $cloudIdManager) {
  29. $this->connection = $connection;
  30. $this->cloudIdManager = $cloudIdManager;
  31. parent::__construct();
  32. }
  33. protected function configure() {
  34. $this
  35. ->setName('sharing:cleanup-remote-storages')
  36. ->setDescription('Cleanup shared storage entries that have no matching entry in the shares_external table')
  37. ->addOption(
  38. 'dry-run',
  39. null,
  40. InputOption::VALUE_NONE,
  41. 'only show which storages would be deleted'
  42. );
  43. }
  44. public function execute(InputInterface $input, OutputInterface $output): int {
  45. $remoteStorages = $this->getRemoteStorages();
  46. $output->writeln(count($remoteStorages) . ' remote storage(s) need(s) to be checked');
  47. $remoteShareIds = $this->getRemoteShareIds();
  48. $output->writeln(count($remoteShareIds) . ' remote share(s) exist');
  49. foreach ($remoteShareIds as $id => $remoteShareId) {
  50. if (isset($remoteStorages[$remoteShareId])) {
  51. if ($input->getOption('dry-run') || $output->isVerbose()) {
  52. $output->writeln("<info>$remoteShareId belongs to remote share $id</info>");
  53. }
  54. unset($remoteStorages[$remoteShareId]);
  55. } else {
  56. $output->writeln("<comment>$remoteShareId for share $id has no matching storage, yet</comment>");
  57. }
  58. }
  59. if (empty($remoteStorages)) {
  60. $output->writeln('<info>no storages deleted</info>');
  61. } else {
  62. $dryRun = $input->getOption('dry-run');
  63. foreach ($remoteStorages as $id => $numericId) {
  64. if ($dryRun) {
  65. $output->writeln("<error>$id [$numericId] can be deleted</error>");
  66. $this->countFiles($numericId, $output);
  67. } else {
  68. $this->deleteStorage($id, $numericId, $output);
  69. }
  70. }
  71. }
  72. return 0;
  73. }
  74. public function countFiles($numericId, OutputInterface $output) {
  75. $queryBuilder = $this->connection->getQueryBuilder();
  76. $queryBuilder->select($queryBuilder->func()->count('fileid'))
  77. ->from('filecache')
  78. ->where($queryBuilder->expr()->eq(
  79. 'storage',
  80. $queryBuilder->createNamedParameter($numericId, IQueryBuilder::PARAM_STR),
  81. IQueryBuilder::PARAM_STR)
  82. );
  83. $result = $queryBuilder->executeQuery();
  84. $count = $result->fetchOne();
  85. $result->closeCursor();
  86. $output->writeln("$count files can be deleted for storage $numericId");
  87. }
  88. public function deleteStorage($id, $numericId, OutputInterface $output) {
  89. $queryBuilder = $this->connection->getQueryBuilder();
  90. $queryBuilder->delete('storages')
  91. ->where($queryBuilder->expr()->eq(
  92. 'id',
  93. $queryBuilder->createNamedParameter($id, IQueryBuilder::PARAM_STR),
  94. IQueryBuilder::PARAM_STR)
  95. );
  96. $output->write("deleting $id [$numericId] ... ");
  97. $count = $queryBuilder->executeStatement();
  98. $output->writeln("deleted $count storage");
  99. $this->deleteFiles($numericId, $output);
  100. }
  101. public function deleteFiles($numericId, OutputInterface $output) {
  102. $queryBuilder = $this->connection->getQueryBuilder();
  103. $queryBuilder->delete('filecache')
  104. ->where($queryBuilder->expr()->eq(
  105. 'storage',
  106. $queryBuilder->createNamedParameter($numericId, IQueryBuilder::PARAM_STR),
  107. IQueryBuilder::PARAM_STR)
  108. );
  109. $output->write("deleting files for storage $numericId ... ");
  110. $count = $queryBuilder->executeStatement();
  111. $output->writeln("deleted $count files");
  112. }
  113. public function getRemoteStorages() {
  114. $queryBuilder = $this->connection->getQueryBuilder();
  115. $queryBuilder->select(['id', 'numeric_id'])
  116. ->from('storages')
  117. ->where($queryBuilder->expr()->like(
  118. 'id',
  119. // match all 'shared::' + 32 characters storages
  120. $queryBuilder->createNamedParameter($this->connection->escapeLikeParameter('shared::') . str_repeat('_', 32)),
  121. IQueryBuilder::PARAM_STR)
  122. )
  123. ->andWhere($queryBuilder->expr()->notLike(
  124. 'id',
  125. // but not the ones starting with a '/', they are for normal shares
  126. $queryBuilder->createNamedParameter($this->connection->escapeLikeParameter('shared::/') . '%'),
  127. IQueryBuilder::PARAM_STR)
  128. )
  129. ->orderBy('numeric_id');
  130. $result = $queryBuilder->executeQuery();
  131. $remoteStorages = [];
  132. while ($row = $result->fetch()) {
  133. $remoteStorages[$row['id']] = $row['numeric_id'];
  134. }
  135. $result->closeCursor();
  136. return $remoteStorages;
  137. }
  138. public function getRemoteShareIds() {
  139. $queryBuilder = $this->connection->getQueryBuilder();
  140. $queryBuilder->select(['id', 'share_token', 'owner', 'remote'])
  141. ->from('share_external');
  142. $result = $queryBuilder->executeQuery();
  143. $remoteShareIds = [];
  144. while ($row = $result->fetch()) {
  145. $cloudId = $this->cloudIdManager->getCloudId($row['owner'], $row['remote']);
  146. $remote = $cloudId->getRemote();
  147. $remoteShareIds[$row['id']] = 'shared::' . md5($row['share_token'] . '@' . $remote);
  148. }
  149. $result->closeCursor();
  150. return $remoteShareIds;
  151. }
  152. }