CleanupRemoteStorages.php 5.3 KB

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