Passed
Push — master ( 2fbb6b...81caef )
by Roeland
12:37 queued 10s
created

RepairTree::execute()   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 41
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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