RepairTree.php 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
  5. *
  6. * @author Robin Appelman <robin@icewind.nl>
  7. *
  8. * @license GNU AGPL version 3 or any later version
  9. *
  10. * This program is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU Affero General Public License as
  12. * published by the Free Software Foundation, either version 3 of the
  13. * License, or (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. *
  23. */
  24. namespace OCA\Files\Command;
  25. use OCP\IDBConnection;
  26. use Symfony\Component\Console\Command\Command;
  27. use Symfony\Component\Console\Input\InputInterface;
  28. use Symfony\Component\Console\Output\OutputInterface;
  29. class RepairTree extends Command {
  30. public const CHUNK_SIZE = 200;
  31. /**
  32. * @var IDBConnection
  33. */
  34. protected $connection;
  35. public function __construct(IDBConnection $connection) {
  36. $this->connection = $connection;
  37. parent::__construct();
  38. }
  39. protected function configure() {
  40. $this
  41. ->setName('files:repair-tree')
  42. ->setDescription('Try and repair malformed filesystem tree structures')
  43. ->addOption('dry-run');
  44. }
  45. public function execute(InputInterface $input, OutputInterface $output): int {
  46. $rows = $this->findBrokenTreeBits();
  47. $fix = !$input->getOption('dry-run');
  48. $output->writeln("Found " . count($rows) . " file entries with an invalid path");
  49. if ($fix) {
  50. $this->connection->beginTransaction();
  51. }
  52. $query = $this->connection->getQueryBuilder();
  53. $query->update('filecache')
  54. ->set('path', $query->createParameter('path'))
  55. ->set('path_hash', $query->func()->md5($query->createParameter('path')))
  56. ->set('storage', $query->createParameter('storage'))
  57. ->where($query->expr()->eq('fileid', $query->createParameter('fileid')));
  58. foreach ($rows as $row) {
  59. $output->writeln("Path of file {$row['fileid']} is {$row['path']} but should be {$row['parent_path']}/{$row['name']} based on its parent", OutputInterface::VERBOSITY_VERBOSE);
  60. if ($fix) {
  61. $fileId = $this->getFileId((int)$row['parent_storage'], $row['parent_path'] . '/' . $row['name']);
  62. if ($fileId > 0) {
  63. $output->writeln("Cache entry has already be recreated with id $fileId, deleting instead");
  64. $this->deleteById((int)$row['fileid']);
  65. } else {
  66. $query->setParameters([
  67. 'fileid' => $row['fileid'],
  68. 'path' => $row['parent_path'] . '/' . $row['name'],
  69. 'storage' => $row['parent_storage'],
  70. ]);
  71. $query->execute();
  72. }
  73. }
  74. }
  75. if ($fix) {
  76. $this->connection->commit();
  77. }
  78. return 0;
  79. }
  80. private function getFileId(int $storage, string $path) {
  81. $query = $this->connection->getQueryBuilder();
  82. $query->select('fileid')
  83. ->from('filecache')
  84. ->where($query->expr()->eq('storage', $query->createNamedParameter($storage)))
  85. ->andWhere($query->expr()->eq('path_hash', $query->createNamedParameter(md5($path))));
  86. return $query->execute()->fetch(\PDO::FETCH_COLUMN);
  87. }
  88. private function deleteById(int $fileId) {
  89. $query = $this->connection->getQueryBuilder();
  90. $query->delete('filecache')
  91. ->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId)));
  92. $query->execute();
  93. }
  94. private function findBrokenTreeBits(): array {
  95. $query = $this->connection->getQueryBuilder();
  96. $query->select('f.fileid', 'f.path', 'f.parent', 'f.name')
  97. ->selectAlias('p.path', 'parent_path')
  98. ->selectAlias('p.storage', 'parent_storage')
  99. ->from('filecache', 'f')
  100. ->innerJoin('f', 'filecache', 'p', $query->expr()->eq('f.parent', 'p.fileid'))
  101. ->where($query->expr()->orX(
  102. $query->expr()->andX(
  103. $query->expr()->neq('p.path_hash', $query->createNamedParameter(md5(''))),
  104. $query->expr()->neq('f.path', $query->func()->concat('p.path', $query->func()->concat($query->createNamedParameter('/'), 'f.name')))
  105. ),
  106. $query->expr()->andX(
  107. $query->expr()->eq('p.path_hash', $query->createNamedParameter(md5(''))),
  108. $query->expr()->neq('f.path', 'f.name')
  109. ),
  110. $query->expr()->neq('f.storage', 'p.storage')
  111. ));
  112. return $query->execute()->fetchAll();
  113. }
  114. }