Completed
Push — master ( 66029f...d05410 )
by
unknown
03:00
created

RepairMismatchFileCachePath::run()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Vincent Petry <[email protected]>
4
 *
5
 * @copyright Copyright (c) 2018, ownCloud GmbH
6
 * @license AGPL-3.0
7
 *
8
 * This code is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License, version 3,
10
 * as published by the Free Software Foundation.
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, version 3,
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
19
 *
20
 */
21
22
namespace OC\Repair;
23
24
use OCP\IConfig;
25
use OCP\ILogger;
26
use OCP\Migration\IOutput;
27
use OCP\Migration\IRepairStep;
28
use Doctrine\DBAL\Platforms\MySqlPlatform;
29
use Doctrine\DBAL\Platforms\OraclePlatform;
30
use OCP\Files\IMimeTypeLoader;
31
use OCP\IDBConnection;
32
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
33
34
/**
35
 * Repairs file cache entry which path do not match the parent-child relationship
36
 */
37
class RepairMismatchFileCachePath implements IRepairStep {
38
	const CHUNK_SIZE = 10000;
39
40
	/** @var IDBConnection */
41
	protected $connection;
42
43
	/** @var IMimeTypeLoader */
44
	protected $mimeLoader;
45
46
	/** @var int */
47
	protected $dirMimeTypeId;
48
49
	/** @var int */
50
	protected $dirMimePartId;
51
52
	/** @var int|null */
53
	protected $storageNumericId = null;
54
55
	/** @var bool */
56
	protected $countOnly = true;
57
58
	/** @var ILogger  */
59
	protected $logger;
60
61
	/** @var IConfig */
62
	protected $config;
63
64
	/**
65
	 * @param \OCP\IDBConnection $connection
66
	 */
67
	public function __construct(IDBConnection $connection,
68
								IMimeTypeLoader $mimeLoader,
69
								ILogger $logger,
70
								IConfig $config) {
71
		$this->connection = $connection;
72
		$this->mimeLoader = $mimeLoader;
73
		$this->logger = $logger;
74
		$this->config = $config;
75
	}
76
77
	public function getName() {
78
		if ($this->countOnly) {
79
			return 'Detect file cache entries with path that does not match parent-child relationships';
80
		} else {
81
			return 'Repair file cache entries with path that does not match parent-child relationships';
82
		}
83
	}
84
85
	/**
86
	 * Sets the numeric id of the storage to process or null to process all.
87
	 *
88
	 * @param int $storageNumericId numeric id of the storage
89
	 */
90
	public function setStorageNumericId($storageNumericId) {
91
		$this->storageNumericId = $storageNumericId;
92
	}
93
94
	/**
95
	 * Sets whether to actually repair or only count entries
96
	 *
97
	 * @param bool $countOnly count only
98
	 */
99
	public function setCountOnly($countOnly) {
100
		$this->countOnly = $countOnly;
101
	}
102
103
	/**
104
	 * Fixes the broken entry's path.
105
	 *
106
	 * @param IOutput $out repair output
107
	 * @param int $fileId file id of the entry to fix
108
	 * @param string $wrongPath wrong path of the entry to fix
109
	 * @param int $correctStorageNumericId numeric idea of the correct storage
110
	 * @param string $correctPath value to which to set the path of the entry
111
	 * @return bool true for success
112
	 */
113
	private function fixEntryPath(IOutput $out, $fileId, $wrongPath, $correctStorageNumericId, $correctPath) {
114
		// delete target if exists
115
		$qb = $this->connection->getQueryBuilder();
116
		$qb->delete('filecache')
117
			->where($qb->expr()->eq('storage', $qb->createNamedParameter($correctStorageNumericId)));
118
119 View Code Duplication
		if ($correctPath === '' && $this->connection->getDatabasePlatform() instanceof OraclePlatform) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
120
			$qb->andWhere($qb->expr()->isNull('path'));
121
		} else {
122
			$qb->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter(\md5($correctPath))));
123
		}
124
		$entryExisted = $qb->execute() > 0;
125
126
		$qb = $this->connection->getQueryBuilder();
127
		$qb->update('filecache')
128
			->set('path', $qb->createNamedParameter($correctPath))
129
			->set('path_hash', $qb->createNamedParameter(\md5($correctPath)))
130
			->set('storage', $qb->createNamedParameter($correctStorageNumericId))
131
			->where($qb->expr()->eq('fileid', $qb->createNamedParameter($fileId)));
132
		$qb->execute();
133
134
		$text = "Fixed file cache entry with fileid $fileId, set wrong path \"$wrongPath\" to \"$correctPath\"";
135
		if ($entryExisted) {
136
			$text = " (replaced an existing entry)";
137
		}
138
		$out->advance(1, $text);
139
	}
140
141
	private function addQueryConditionsParentIdWrongPath($qb) {
142
		// thanks, VicDeo!
143
		if ($this->connection->getDatabasePlatform() instanceof MySqlPlatform) {
144
			$concatFunction = $qb->createFunction("CONCAT(fcp.path, '/', fc.name)");
145
		} else {
146
			$concatFunction = $qb->createFunction("(fcp.`path` || '/' || fc.`name`)");
147
		}
148
149
		if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
150
			$emptyPathExpr = $qb->expr()->isNotNull('fcp.path');
151
		} else {
152
			$emptyPathExpr = $qb->expr()->neq('fcp.path', $qb->expr()->literal(''));
153
		}
154
155
		$qb
156
			->from('filecache', 'fc')
157
			->from('filecache', 'fcp')
158
			->where($qb->expr()->eq('fc.parent', 'fcp.fileid'))
159
			->andWhere(
160
				$qb->expr()->orX(
161
					$qb->expr()->neq(
162
						$qb->createFunction($concatFunction),
163
						'fc.path'
164
					),
165
					$qb->expr()->neq('fc.storage', 'fcp.storage')
166
				)
167
			)
168
			->andWhere($emptyPathExpr)
169
			// yes, this was observed in the wild...
170
			->andWhere($qb->expr()->neq('fc.fileid', 'fcp.fileid'));
171
172
		if ($this->storageNumericId !== null) {
173
			// use the target storage of the failed move when filtering
174
			$qb->andWhere(
175
				$qb->expr()->eq('fc.storage', $qb->createNamedParameter($this->storageNumericId))
176
			);
177
		}
178
	}
179
180
	private function addQueryConditionsNonExistingParentIdEntry($qb, $storageNumericId = null) {
181
		// Subquery for parent existence
182
		$qbe = $this->connection->getQueryBuilder();
183
		$qbe->select($qbe->expr()->literal('1'))
184
			->from('filecache', 'fce')
185
			->where($qbe->expr()->eq('fce.fileid', 'fc.parent'));
186
187
		// Find entries to repair
188
		// select fc.storage,fc.fileid,fc.parent as "wrongparent",fc.path,fc.etag
189
		// and not exists (select 1 from oc_filecache fc2 where fc2.fileid = fc.parent)
190
		$qb->select('storage', 'fileid', 'path', 'parent')
191
			// from oc_filecache fc
192
			->from('filecache', 'fc')
193
			// where fc.parent <> -1
194
			->where($qb->expr()->neq('fc.parent', $qb->createNamedParameter(-1)))
195
			// and not exists (select 1 from oc_filecache fc2 where fc2.fileid = fc.parent)
196
			->andWhere(
197
				$qb->expr()->orX(
198
					$qb->expr()->eq('fc.fileid', 'fc.parent'),
199
					$qb->createFunction('NOT EXISTS (' . $qbe->getSQL() . ')')
200
				)
201
			);
202
203
		if ($storageNumericId !== null) {
204
			// filter on destination storage of a failed move
205
			$qb->andWhere($qb->expr()->eq('fc.storage', $qb->createNamedParameter($storageNumericId)));
206
		}
207
	}
208
209 View Code Duplication
	private function countResultsToProcessParentIdWrongPath($storageNumericId = null) {
210
		$qb = $this->connection->getQueryBuilder();
211
		$qb->select($qb->createFunction('COUNT(*)'));
212
		$this->addQueryConditionsParentIdWrongPath($qb, $storageNumericId);
0 ignored issues
show
Unused Code introduced by
The call to RepairMismatchFileCacheP...ionsParentIdWrongPath() has too many arguments starting with $storageNumericId.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
213
		$results = $qb->execute();
214
		$count = $results->fetchColumn(0);
215
		$results->closeCursor();
216
		return $count;
217
	}
218
219 View Code Duplication
	private function countResultsToProcessNonExistingParentIdEntry($storageNumericId = null) {
220
		$qb = $this->connection->getQueryBuilder();
221
		$qb->select($qb->createFunction('COUNT(*)'));
222
		$this->addQueryConditionsNonExistingParentIdEntry($qb, $storageNumericId);
223
		$results = $qb->execute();
224
		$count = $results->fetchColumn(0);
225
		$results->closeCursor();
226
		return $count;
227
	}
228
229
	/**
230
	 * Outputs a report about storages with wrong path that need repairing in the file cache
231
	 */
232 View Code Duplication
	private function reportAffectedStoragesParentIdWrongPath(IOutput $out) {
233
		$qb = $this->connection->getQueryBuilder();
234
		$qb->selectDistinct('fc.storage');
235
		$this->addQueryConditionsParentIdWrongPath($qb);
236
237
		// TODO: max results + paginate ?
238
		// TODO: join with oc_storages / oc_mounts to deliver user id ?
239
240
		$results = $qb->execute();
241
		$rows = $results->fetchAll();
242
		$results->closeCursor();
243
244
		$storageIds = [];
245
		foreach ($rows as $row) {
246
			$storageIds[] = $row['storage'];
247
		}
248
249
		if (!empty($storageIds)) {
250
			$out->warning('The file cache contains entries with invalid path values for the following storage numeric ids: ' . \implode(' ', $storageIds));
251
			$out->warning('Please run `occ files:scan --all --repair` to repair'
252
			.'all affected storages or run `occ files:scan userid --repair for '
253
			.'each user with affected storages');
254
		}
255
	}
256
257
	/**
258
	 * Outputs a report about storages with non existing parents that need repairing in the file cache
259
	 */
260 View Code Duplication
	private function reportAffectedStoragesNonExistingParentIdEntry(IOutput $out) {
261
		$qb = $this->connection->getQueryBuilder();
262
		$qb->selectDistinct('fc.storage');
263
		$this->addQueryConditionsNonExistingParentIdEntry($qb);
264
265
		// TODO: max results + paginate ?
266
		// TODO: join with oc_storages / oc_mounts to deliver user id ?
267
268
		$results = $qb->execute();
269
		$rows = $results->fetchAll();
270
		$results->closeCursor();
271
272
		$storageIds = [];
273
		foreach ($rows as $row) {
274
			$storageIds[] = $row['storage'];
275
		}
276
277
		if (!empty($storageIds)) {
278
			$out->warning('The file cache contains entries where the parent id does not point to any existing entry for the following storage numeric ids: ' . \implode(' ', $storageIds));
279
			$out->warning('Please run `occ files:scan --all --repair` to repair all affected storages');
280
		}
281
	}
282
283
	/**
284
	 * Repair all entries for which the parent entry exists but the path
285
	 * value doesn't match the parent's path.
286
	 *
287
	 * @param IOutput $out
288
	 * @param int|null $storageNumericId storage to fix or null for all
289
	 * @return int[] storage numeric ids that were targets to a move and needs further fixing
290
	 */
291
	private function fixEntriesWithCorrectParentIdButWrongPath(IOutput $out, $storageNumericId = null) {
292
		$totalResultsCount = 0;
293
		$affectedStorages = [$storageNumericId => true];
294
295
		// find all entries where the path entry doesn't match the path value that would
296
		// be expected when following the parent-child relationship, basically
297
		// concatenating the parent's "path" value with the name of the child
298
		$qb = $this->connection->getQueryBuilder();
299
		$qb->select('fc.storage', 'fc.fileid', 'fc.name')
0 ignored issues
show
Unused Code introduced by
The call to IQueryBuilder::select() has too many arguments starting with 'fc.fileid'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
300
			->selectAlias('fc.path', 'path')
301
			->selectAlias('fc.parent', 'wrongparentid')
302
			->selectAlias('fcp.storage', 'parentstorage')
303
			->selectAlias('fcp.path', 'parentpath');
304
		$this->addQueryConditionsParentIdWrongPath($qb, $storageNumericId);
0 ignored issues
show
Unused Code introduced by
The call to RepairMismatchFileCacheP...ionsParentIdWrongPath() has too many arguments starting with $storageNumericId.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
305
		$qb->setMaxResults(self::CHUNK_SIZE);
306
307
		do {
308
			$results = $qb->execute();
309
			// since we're going to operate on fetched entry, better cache them
310
			// to avoid DB lock ups
311
			$rows = $results->fetchAll();
312
			$results->closeCursor();
313
314
			$this->connection->beginTransaction();
315
			$lastResultsCount = 0;
316
			foreach ($rows as $row) {
317
				$wrongPath = $row['path'];
318
				$correctPath = $row['parentpath'] . '/' . $row['name'];
319
				// make sure the target is on a different subtree
320
				if (\substr($correctPath, 0, \strlen($wrongPath)) === $wrongPath) {
321
					// the path based parent entry is referencing one of its own children,
322
					// fix the entry's parent id instead
323
					// note: fixEntryParent cannot fail to find the parent entry by path
324
					// here because the reason we reached this code is because we already
325
					// found it
326
					$this->fixEntryParent(
327
						$out,
328
						$row['storage'],
329
						$row['fileid'],
330
						$row['path'],
331
						$row['wrongparentid'],
332
						true
333
					);
334
				} else {
335
					$this->fixEntryPath(
336
						$out,
337
						$row['fileid'],
338
						$wrongPath,
339
						$row['parentstorage'],
340
						$correctPath
341
					);
342
					// we also need to fix the target storage
343
					$affectedStorages[$row['parentstorage']] = true;
344
				}
345
				$lastResultsCount++;
346
			}
347
			$this->connection->commit();
348
349
			$totalResultsCount += $lastResultsCount;
350
351
			// note: this is not pagination but repeating the query over and over again
352
			// until all possible entries were fixed
353
		} while ($lastResultsCount > 0);
354
355
		if ($totalResultsCount > 0) {
356
			$out->info("Fixed $totalResultsCount file cache entries with wrong path");
357
		}
358
359
		return \array_keys($affectedStorages);
360
	}
361
362
	/**
363
	 * Gets the file id of the entry. If none exists, create it
364
	 * up to the root if needed.
365
	 *
366
	 * @param int $storageId storage id
367
	 * @param string $path path for which to create the parent entry
368
	 * @return int file id of the newly created parent
369
	 */
370
	private function getOrCreateEntry($storageId, $path, $reuseFileId = null) {
371
		if ($path === '.') {
372
			$path = '';
373
		}
374
		// find the correct parent
375
		$qb = $this->connection->getQueryBuilder();
376
		// select fileid as "correctparentid"
377
		$qb->select('fileid')
378
			// from oc_filecache
379
			->from('filecache')
380
			// where storage=$storage and path='$parentPath'
381
			->where($qb->expr()->eq('storage', $qb->createNamedParameter($storageId)));
382
383 View Code Duplication
		if ($path === '' && $this->connection->getDatabasePlatform() instanceof OraclePlatform) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
384
			$qb->andWhere($qb->expr()->isNull('path'));
385
		} else {
386
			$qb->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter(\md5($path))));
387
		}
388
		$results = $qb->execute();
389
		$rows = $results->fetchAll();
390
		$results->closeCursor();
391
392
		if (!empty($rows)) {
393
			return $rows[0]['fileid'];
394
		}
395
396
		if ($path !== '') {
397
			$parentId = $this->getOrCreateEntry($storageId, \dirname($path));
398
		} else {
399
			// root entry missing, create it
400
			$parentId = -1;
401
		}
402
403
		$qb = $this->connection->getQueryBuilder();
404
		$values = [
405
			'storage' => $qb->createNamedParameter($storageId),
406
			'path' => $qb->createNamedParameter($path),
407
			'path_hash' => $qb->createNamedParameter(\md5($path)),
408
			'name' => $qb->createNamedParameter(\basename($path)),
409
			'parent' => $qb->createNamedParameter($parentId),
410
			'size' => $qb->createNamedParameter(-1),
411
			'etag' => $qb->createNamedParameter('zombie'),
412
			'mimetype' => $qb->createNamedParameter($this->dirMimeTypeId),
413
			'mimepart' => $qb->createNamedParameter($this->dirMimePartId),
414
		];
415
416
		if ($reuseFileId !== null) {
417
			// purpose of reusing the fileid of the parent is to salvage potential
418
			// metadata that might have previously been linked to this file id
419
			$values['fileid'] = $qb->createNamedParameter($reuseFileId);
420
		}
421
		$qb->insert('filecache')->values($values);
422
		try {
423
			$qb->execute();
424
		} catch (UniqueConstraintViolationException $e) {
425
			// This situation should no happen - need debugging information if it does
426
			\OC::$server->getLogger()->logException($e);
427
			\OC::$server->getLogger()->error("Filecache repair step tried to insert row that already existed with fileid: {$values['fileid']}");
428
			// Skip if the entry already exists
429
		}
430
431
		// If we reused the fileid then this is the id to return
432
		if ($reuseFileId !== null) {
433
			// with Oracle, the trigger gets in the way and does not let us specify
434
			// a fileid value on insert
435
			if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
436
				$lastFileId = $this->connection->lastInsertId('*PREFIX*filecache');
437
				if ($reuseFileId !== $lastFileId) {
438
					// use update to set it directly
439
					$qb = $this->connection->getQueryBuilder();
440
					$qb->update('filecache')
441
						->set('fileid', $qb->createNamedParameter($reuseFileId))
442
						->where($qb->expr()->eq('fileid', $qb->createNamedParameter($lastFileId)));
443
					$qb->execute();
444
				}
445
			}
446
447
			return $reuseFileId;
448
		} else {
449
			// Else we inserted a new row with auto generated id, use that
450
			return $this->connection->lastInsertId('*PREFIX*filecache');
451
		}
452
	}
453
454
	/**
455
	 * Fixes the broken entry's path.
456
	 *
457
	 * @param IOutput $out repair output
458
	 * @param int $storageId storage id of the entry to fix
459
	 * @param int $fileId file id of the entry to fix
460
	 * @param string $path path from the entry to fix
461
	 * @param int $wrongParentId wrong parent id
462
	 * @param bool $parentIdExists true if the entry from the $wrongParentId exists (but is the wrong one),
463
	 * false if it doesn't
464
	 * @return bool true if the entry was fixed, false otherwise
465
	 */
466
	private function fixEntryParent(IOutput $out, $storageId, $fileId, $path, $wrongParentId, $parentIdExists = false) {
467
		if (!$parentIdExists) {
468
			// if the parent doesn't exist, let us reuse its id in case there is metadata to salvage
469
			$correctParentId = $this->getOrCreateEntry($storageId, \dirname($path), $wrongParentId);
470
		} else {
471
			// parent exists and is the wrong one, so recreating would need a new fileid
472
			$correctParentId = $this->getOrCreateEntry($storageId, \dirname($path));
473
		}
474
475
		$this->connection->beginTransaction();
476
477
		$qb = $this->connection->getQueryBuilder();
478
		$qb->update('filecache')
479
			->set('parent', $qb->createNamedParameter($correctParentId))
480
			->where($qb->expr()->eq('fileid', $qb->createNamedParameter($fileId)));
481
		$qb->execute();
482
483
		$text = "Fixed file cache entry with fileid $fileId, set wrong parent \"$wrongParentId\" to \"$correctParentId\"";
484
		$out->advance(1, $text);
485
486
		$this->connection->commit();
487
488
		return true;
489
	}
490
491
	/**
492
	 * Repair entries where the parent id doesn't point to any existing entry
493
	 * by finding the actual parent entry matching the entry's path dirname.
494
	 *
495
	 * @param IOutput $out output
496
	 * @param int|null $storageNumericId storage to fix or null for all
497
	 * @return int number of results that were fixed
498
	 */
499
	private function fixEntriesWithNonExistingParentIdEntry(IOutput $out, $storageNumericId = null) {
500
		$qb = $this->connection->getQueryBuilder();
501
		$this->addQueryConditionsNonExistingParentIdEntry($qb, $storageNumericId);
502
		$qb->setMaxResults(self::CHUNK_SIZE);
503
504
		$totalResultsCount = 0;
505
		do {
506
			$results = $qb->execute();
507
			// since we're going to operate on fetched entry, better cache them
508
			// to avoid DB lock ups
509
			$rows = $results->fetchAll();
510
			$results->closeCursor();
511
512
			$lastResultsCount = 0;
513
			foreach ($rows as $row) {
514
				$this->fixEntryParent(
515
					$out,
516
					$row['storage'],
517
					$row['fileid'],
518
					$row['path'],
519
					$row['parent'],
520
					// in general the parent doesn't exist except
521
					// for the one condition where parent=fileid
522
					$row['parent'] === $row['fileid']
523
				);
524
				$lastResultsCount++;
525
			}
526
527
			$totalResultsCount += $lastResultsCount;
528
529
			// note: this is not pagination but repeating the query over and over again
530
			// until all possible entries were fixed
531
		} while ($lastResultsCount > 0);
532
533
		if ($totalResultsCount > 0) {
534
			$out->info("Fixed $totalResultsCount file cache entries with wrong path");
535
		}
536
537
		return $totalResultsCount;
538
	}
539
540
	/**
541
	 * The purpose of this function is to let execute the run method
542
	 * irrespective of version. For example when triggered from files:scan
543
	 * this repair step shouldn't be blocked.
544
	 *
545
	 * @param IOutput $out
546
	 */
547
	public function doRepair(IOutput $out) {
548
		$this->dirMimeTypeId = $this->mimeLoader->getId('httpd/unix-directory');
549
		$this->dirMimePartId = $this->mimeLoader->getId('httpd');
550
551
		if ($this->countOnly) {
552
			$this->reportAffectedStoragesParentIdWrongPath($out);
553
			$this->reportAffectedStoragesNonExistingParentIdEntry($out);
554
		} else {
555
			$brokenPathEntries = $this->countResultsToProcessParentIdWrongPath($this->storageNumericId);
556
			$brokenParentIdEntries = $this->countResultsToProcessNonExistingParentIdEntry($this->storageNumericId);
557
			$out->startProgress($brokenPathEntries + $brokenParentIdEntries);
558
559
			$totalFixed = 0;
560
561
			/*
562
			 * This repair itself might overwrite existing target parent entries and create
563
			 * orphans where the parent entry of the parent id doesn't exist but the path matches.
564
			 * This needs to be repaired by fixEntriesWithNonExistingParentIdEntry(), this is why
565
			 * we need to keep this specific order of repair.
566
			 */
567
			$affectedStorages = $this->fixEntriesWithCorrectParentIdButWrongPath($out, $this->storageNumericId);
568
569
			if ($this->storageNumericId !== null) {
570
				foreach ($affectedStorages as $storageNumericId) {
571
					$this->fixEntriesWithNonExistingParentIdEntry($out, $storageNumericId);
572
				}
573
			} else {
574
				// just fix all
575
				$this->fixEntriesWithNonExistingParentIdEntry($out);
576
			}
577
			$out->finishProgress();
578
			$out->info('');
579
		}
580
	}
581
582
	/**
583
	 * Run the repair step
584
	 *
585
	 * @param IOutput $out output
586
	 */
587
	public function run(IOutput $out) {
588
		$currentVersion = $this->config->getSystemValue('version', '0.0.0');
589
		$versionCompareStatus = \version_compare($currentVersion, '10.0.4', '<');
590
		//Execute repair step if version is less than 10.0.4 during upgrade
591
		//This is not applicable when called from file scan command
592
		if ($versionCompareStatus) {
593
			$this->doRepair($out);
594
		}
595
	}
596
}
597