Completed
Push — master ( e95e7d...73554a )
by Robin
15:11
created

RepairInvalidPaths::getInvalidEntries()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 29
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 21
nc 2
nop 0
dl 0
loc 29
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Robin Appelman <[email protected]>
4
 *
5
 * @license GNU AGPL version 3 or any later version
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as
9
 * published by the Free Software Foundation, either version 3 of the
10
 * License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
 *
20
 */
21
22
namespace OC\Repair\NC13;
23
24
25
use OCP\DB\QueryBuilder\IQueryBuilder;
26
use OCP\IConfig;
27
use OCP\IDBConnection;
28
use OCP\Migration\IOutput;
29
use OCP\Migration\IRepairStep;
30
31
class RepairInvalidPaths implements IRepairStep {
32
	const MAX_ROWS = 1000;
33
34
	/** @var IDBConnection */
35
	private $connection;
36
	/** @var IConfig */
37
	private $config;
38
39
	private $getIdQuery;
40
	private $updateQuery;
41
	private $reparentQuery;
42
	private $deleteQuery;
43
44
	public function __construct(IDBConnection $connection, IConfig $config) {
45
		$this->connection = $connection;
46
		$this->config = $config;
47
	}
48
49
50
	public function getName() {
51
		return 'Repair invalid paths in file cache';
52
	}
53
54
	/**
55
	 * @return \Generator
56
	 * @suppress SqlInjectionChecker
57
	 */
58
	private function getInvalidEntries() {
59
		$builder = $this->connection->getQueryBuilder();
60
61
		$computedPath = $builder->func()->concat(
62
			'p.path',
63
			$builder->func()->concat($builder->createNamedParameter('/'), 'f.name')
64
		);
65
66
		//select f.path, f.parent,p.path from oc_filecache f inner join oc_filecache p on f.parent=p.fileid and p.path!='' where f.path != p.path || '/' || f.name;
67
		$builder->select('f.fileid', 'f.path', 'f.name', 'f.parent', 'f.storage')
68
			->selectAlias('p.path', 'parent_path')
69
			->selectAlias('p.storage', 'parent_storage')
70
			->from('filecache', 'f')
71
			->innerJoin('f', 'filecache', 'p', $builder->expr()->andX(
0 ignored issues
show
Documentation introduced by
$builder->expr()->andX($...nEmptyString('p.name')) is of type object<OCP\DB\QueryBuilder\ICompositeExpression>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
72
				$builder->expr()->eq('f.parent', 'p.fileid'),
73
				$builder->expr()->nonEmptyString('p.name')
74
			))
75
			->where($builder->expr()->neq('f.path', $computedPath))
76
			->setMaxResults(self::MAX_ROWS);
77
78
		do {
79
			$result = $builder->execute();
80
			$rows = $result->fetchAll();
81
			foreach ($rows as $row) {
82
				yield $row;
83
			}
84
			$result->closeCursor();
85
		} while (count($rows) > 0);
86
	}
87
88
	private function getId($storage, $path) {
89
		if (!$this->getIdQuery) {
90
			$builder = $this->connection->getQueryBuilder();
91
92
			$this->getIdQuery = $builder->select('fileid')
93
				->from('filecache')
94
				->where($builder->expr()->eq('storage', $builder->createParameter('storage')))
95
				->andWhere($builder->expr()->eq('path_hash', $builder->createParameter('path_hash')));
96
		}
97
98
		$this->getIdQuery->setParameter('storage', $storage, IQueryBuilder::PARAM_INT);
99
		$this->getIdQuery->setParameter('path_hash', md5($path));
100
101
		return $this->getIdQuery->execute()->fetchColumn();
102
	}
103
104
	/**
105
	 * @param string $fileid
106
	 * @param string $newPath
107
	 * @param string $newStorage
108
	 * @suppress SqlInjectionChecker
109
	 */
110
	private function update($fileid, $newPath, $newStorage) {
111
		if (!$this->updateQuery) {
112
			$builder = $this->connection->getQueryBuilder();
113
114
			$this->updateQuery = $builder->update('filecache')
115
				->set('path', $builder->createParameter('newpath'))
116
				->set('path_hash', $builder->func()->md5($builder->createParameter('newpath')))
117
				->set('storage', $builder->createParameter('newstorage'))
118
				->where($builder->expr()->eq('fileid', $builder->createParameter('fileid')));
119
		}
120
121
		$this->updateQuery->setParameter('newpath', $newPath);
122
		$this->updateQuery->setParameter('newstorage', $newStorage);
123
		$this->updateQuery->setParameter('fileid', $fileid, IQueryBuilder::PARAM_INT);
124
125
		$this->updateQuery->execute();
126
	}
127
128
	private function reparent($from, $to) {
129 View Code Duplication
		if (!$this->reparentQuery) {
130
			$builder = $this->connection->getQueryBuilder();
131
132
			$this->reparentQuery = $builder->update('filecache')
133
				->set('parent', $builder->createParameter('to'))
134
				->where($builder->expr()->eq('fileid', $builder->createParameter('from')));
135
		}
136
137
		$this->reparentQuery->setParameter('from', $from);
138
		$this->reparentQuery->setParameter('to', $to);
139
140
		$this->reparentQuery->execute();
141
	}
142
143
	private function delete($fileid) {
144 View Code Duplication
		if (!$this->deleteQuery) {
145
			$builder = $this->connection->getQueryBuilder();
146
147
			$this->deleteQuery = $builder->delete('filecache')
148
				->where($builder->expr()->eq('fileid', $builder->createParameter('fileid')));
149
		}
150
151
		$this->deleteQuery->setParameter('fileid', $fileid, IQueryBuilder::PARAM_INT);
152
153
		$this->deleteQuery->execute();
154
	}
155
156
	private function repair() {
157
		$this->connection->beginTransaction();
158
		$entries = $this->getInvalidEntries();
159
		$count = 0;
160
		foreach ($entries as $entry) {
161
			$count++;
162
			$calculatedPath = $entry['parent_path'] . '/' . $entry['name'];
163
			if ($newId = $this->getId($entry['parent_storage'], $calculatedPath)) {
164
				// a new entry with the correct path has already been created, reuse that one and delete the incorrect entry
165
				$this->reparent($entry['fileid'], $newId);
166
				$this->delete($entry['fileid']);
167
			} else {
168
				$this->update($entry['fileid'], $calculatedPath, $entry['parent_storage']);
169
			}
170
		}
171
		$this->connection->commit();
172
		return $count;
173
	}
174
175
	private function shouldRun() {
176
		$versionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0');
177
178
		// was added to 11.0.5.2, 12.0.0.30 and 13.0.0.1
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
179
		$shouldRun = version_compare($versionFromBeforeUpdate, '11.0.5.2', '<');
180
		$shouldRun |= version_compare($versionFromBeforeUpdate, '12.0.0.0', '>=') && version_compare($versionFromBeforeUpdate, '12.0.0.30', '<');
181
		$shouldRun |= version_compare($versionFromBeforeUpdate, '13.0.0.0', '==');
182
		return $shouldRun;
183
	}
184
185
	public function run(IOutput $output) {
186
		if ($this->shouldRun()) {
187
			$count = $this->repair();
188
189
			$output->info('Repaired ' . $count . ' paths');
190
		}
191
	}
192
}
193