Completed
Push — master ( 0eec7a...2e46cb )
by Phil
10:40
created

RepairMismatchFileCachePath::getOrCreateEntry()   D

Complexity

Conditions 11
Paths 132

Size

Total Lines 83
Code Lines 51

Duplication

Lines 5
Ratio 6.02 %

Importance

Changes 0
Metric Value
cc 11
eloc 51
nc 132
nop 3
dl 5
loc 83
rs 4.9629
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\ILogger;
25
use OCP\Migration\IOutput;
26
use OCP\Migration\IRepairStep;
27
use Doctrine\DBAL\Platforms\MySqlPlatform;
28
use Doctrine\DBAL\Platforms\OraclePlatform;
29
use OCP\Files\IMimeTypeLoader;
30
use OCP\IDBConnection;
31
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
32
33
/**
34
 * Repairs file cache entry which path do not match the parent-child relationship
35
 */
36
class RepairMismatchFileCachePath implements IRepairStep {
37
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
	/**
62
	 * @param \OCP\IDBConnection $connection
63
	 */
64
	public function __construct(IDBConnection $connection,
65
								IMimeTypeLoader $mimeLoader,
66
								ILogger $logger) {
67
		$this->connection = $connection;
68
		$this->mimeLoader = $mimeLoader;
69
		$this->logger = $logger;
70
	}
71
72
	public function getName() {
73
		if ($this->countOnly) {
74
			return 'Detect file cache entries with path that does not match parent-child relationships';
75
		} else {
76
			return 'Repair file cache entries with path that does not match parent-child relationships';
77
		}
78
	}
79
80
	/**
81
	 * Sets the numeric id of the storage to process or null to process all.
82
	 *
83
	 * @param int $storageNumericId numeric id of the storage
84
	 */
85
	public function setStorageNumericId($storageNumericId) {
86
		$this->storageNumericId = $storageNumericId;
87
	}
88
89
	/**
90
	 * Sets whether to actually repair or only count entries
91
	 *
92
	 * @param bool $countOnly count only
93
	 */
94
	public function setCountOnly($countOnly) {
95
		$this->countOnly = $countOnly;
96
	}
97
98
	/**
99
	 * Fixes the broken entry's path.
100
	 *
101
	 * @param IOutput $out repair output
102
	 * @param int $fileId file id of the entry to fix
103
	 * @param string $wrongPath wrong path of the entry to fix
104
	 * @param int $correctStorageNumericId numeric idea of the correct storage
105
	 * @param string $correctPath value to which to set the path of the entry 
106
	 * @return bool true for success
107
	 */
108
	private function fixEntryPath(IOutput $out, $fileId, $wrongPath, $correctStorageNumericId, $correctPath) {
109
		// delete target if exists
110
		$qb = $this->connection->getQueryBuilder();
111
		$qb->delete('filecache')
112
			->where($qb->expr()->eq('storage', $qb->createNamedParameter($correctStorageNumericId)));
113
114 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...
115
			$qb->andWhere($qb->expr()->isNull('path'));
116
		} else {
117
			$qb->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter(md5($correctPath))));
118
		}
119
		$entryExisted = $qb->execute() > 0;
120
121
		$qb = $this->connection->getQueryBuilder();
122
		$qb->update('filecache')
123
			->set('path', $qb->createNamedParameter($correctPath))
124
			->set('path_hash', $qb->createNamedParameter(md5($correctPath)))
125
			->set('storage', $qb->createNamedParameter($correctStorageNumericId))
126
			->where($qb->expr()->eq('fileid', $qb->createNamedParameter($fileId)));
127
		$qb->execute();
128
129
		$text = "Fixed file cache entry with fileid $fileId, set wrong path \"$wrongPath\" to \"$correctPath\"";
130
		if ($entryExisted) {
131
			$text = " (replaced an existing entry)";
132
		}
133
		$out->advance(1, $text);
134
	}
135
136
	private function addQueryConditionsParentIdWrongPath($qb) {
137
		// thanks, VicDeo!
138
		if ($this->connection->getDatabasePlatform() instanceof MySqlPlatform) {
139
			$concatFunction = $qb->createFunction("CONCAT(fcp.path, '/', fc.name)");
140
		} else {
141
			$concatFunction = $qb->createFunction("(fcp.`path` || '/' || fc.`name`)");
142
		}
143
144
		if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
145
			$emptyPathExpr = $qb->expr()->isNotNull('fcp.path');
146
		} else {
147
			$emptyPathExpr = $qb->expr()->neq('fcp.path', $qb->expr()->literal(''));
148
		}
149
150
		$qb
151
			->from('filecache', 'fc')
152
			->from('filecache', 'fcp')
153
			->where($qb->expr()->eq('fc.parent', 'fcp.fileid'))
154
			->andWhere(
155
				$qb->expr()->orX(
156
					$qb->expr()->neq(
157
						$qb->createFunction($concatFunction),
158
						'fc.path'
159
					),
160
					$qb->expr()->neq('fc.storage', 'fcp.storage')
161
				)
162
			)
163
			->andWhere($emptyPathExpr)
164
			// yes, this was observed in the wild...
165
			->andWhere($qb->expr()->neq('fc.fileid', 'fcp.fileid'));
166
167
		if ($this->storageNumericId !== null) {
168
			// use the target storage of the failed move when filtering
169
			$qb->andWhere(
170
				$qb->expr()->eq('fc.storage', $qb->createNamedParameter($this->storageNumericId))
171
			);
172
		}
173
	}
174
175
	private function addQueryConditionsNonExistingParentIdEntry($qb, $storageNumericId = null) {
176
		// Subquery for parent existence
177
		$qbe = $this->connection->getQueryBuilder();
178
		$qbe->select($qbe->expr()->literal('1'))
179
			->from('filecache', 'fce')
180
			->where($qbe->expr()->eq('fce.fileid', 'fc.parent'));
181
182
		// Find entries to repair
183
		// select fc.storage,fc.fileid,fc.parent as "wrongparent",fc.path,fc.etag
184
		// and not exists (select 1 from oc_filecache fc2 where fc2.fileid = fc.parent)
185
		$qb->select('storage', 'fileid', 'path', 'parent')
186
			// from oc_filecache fc
187
			->from('filecache', 'fc')
188
			// where fc.parent <> -1
189
			->where($qb->expr()->neq('fc.parent', $qb->createNamedParameter(-1)))
190
			// and not exists (select 1 from oc_filecache fc2 where fc2.fileid = fc.parent)
191
			->andWhere(
192
				$qb->expr()->orX(
193
					$qb->expr()->eq('fc.fileid', 'fc.parent'),
194
					$qb->createFunction('NOT EXISTS (' . $qbe->getSQL() . ')')
195
				)
196
			);
197
198
		if ($storageNumericId !== null) {
199
			// filter on destination storage of a failed move
200
			$qb->andWhere($qb->expr()->eq('fc.storage', $qb->createNamedParameter($storageNumericId)));
201
		}
202
	}
203
204 View Code Duplication
	private function countResultsToProcessParentIdWrongPath($storageNumericId = null) {
205
		$qb = $this->connection->getQueryBuilder();
206
		$qb->select($qb->createFunction('COUNT(*)'));
207
		$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...
208
		$results = $qb->execute();
209
		$count = $results->fetchColumn(0);
210
		$results->closeCursor();
211
		return $count;
212
	}
213
214 View Code Duplication
	private function countResultsToProcessNonExistingParentIdEntry($storageNumericId = null) {
215
		$qb = $this->connection->getQueryBuilder();
216
		$qb->select($qb->createFunction('COUNT(*)'));
217
		$this->addQueryConditionsNonExistingParentIdEntry($qb, $storageNumericId);
218
		$results = $qb->execute();
219
		$count = $results->fetchColumn(0);
220
		$results->closeCursor();
221
		return $count;
222
	}
223
224
225
	/**
226
	 * Outputs a report about storages with wrong path that need repairing in the file cache
227
	 */
228 View Code Duplication
	private function reportAffectedStoragesParentIdWrongPath(IOutput $out) {
229
		$qb = $this->connection->getQueryBuilder();
230
		$qb->selectDistinct('fc.storage');
231
		$this->addQueryConditionsParentIdWrongPath($qb);
232
233
		// TODO: max results + paginate ?
234
		// TODO: join with oc_storages / oc_mounts to deliver user id ?
235
236
		$results = $qb->execute();
237
		$rows = $results->fetchAll();
238
		$results->closeCursor();
239
240
		$storageIds = [];
241
		foreach ($rows as $row) {
242
			$storageIds[] = $row['storage'];
243
		}
244
245
		if (!empty($storageIds)) {
246
			$out->warning('The file cache contains entries with invalid path values for the following storage numeric ids: ' . implode(' ', $storageIds));
247
			$out->warning('Please run `occ files:scan --all --repair` to repair'
248
			.'all affected storages or run `occ files:scan userid --repair for '
249
			.'each user with affected storages');
250
		}
251
	}
252
253
	/**
254
	 * Outputs a report about storages with non existing parents that need repairing in the file cache
255
	 */
256 View Code Duplication
	private function reportAffectedStoragesNonExistingParentIdEntry(IOutput $out) {
257
		$qb = $this->connection->getQueryBuilder();
258
		$qb->selectDistinct('fc.storage');
259
		$this->addQueryConditionsNonExistingParentIdEntry($qb);
260
261
		// TODO: max results + paginate ?
262
		// TODO: join with oc_storages / oc_mounts to deliver user id ?
263
264
		$results = $qb->execute();
265
		$rows = $results->fetchAll();
266
		$results->closeCursor();
267
268
		$storageIds = [];
269
		foreach ($rows as $row) {
270
			$storageIds[] = $row['storage'];
271
		}
272
273
		if (!empty($storageIds)) {
274
			$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));
275
			$out->warning('Please run `occ files:scan --all --repair` to repair all affected storages');
276
		}
277
	}
278
279
	/**
280
	 * Repair all entries for which the parent entry exists but the path
281
	 * value doesn't match the parent's path.
282
	 *
283
	 * @param IOutput $out
284
	 * @param int|null $storageNumericId storage to fix or null for all
285
	 * @return int[] storage numeric ids that were targets to a move and needs further fixing
286
	 */
287
	private function fixEntriesWithCorrectParentIdButWrongPath(IOutput $out, $storageNumericId = null) {
288
		$totalResultsCount = 0;
289
		$affectedStorages = [$storageNumericId => true];
290
291
		// find all entries where the path entry doesn't match the path value that would
292
		// be expected when following the parent-child relationship, basically
293
		// concatenating the parent's "path" value with the name of the child
294
		$qb = $this->connection->getQueryBuilder();
295
		$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...
296
			->selectAlias('fc.path', 'path')
297
			->selectAlias('fc.parent', 'wrongparentid')
298
			->selectAlias('fcp.storage', 'parentstorage')
299
			->selectAlias('fcp.path', 'parentpath');
300
		$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...
301
		$qb->setMaxResults(self::CHUNK_SIZE);
302
303
		do {
304
			$results = $qb->execute();
305
			// since we're going to operate on fetched entry, better cache them
306
			// to avoid DB lock ups
307
			$rows = $results->fetchAll();
308
			$results->closeCursor();
309
310
			$this->connection->beginTransaction();
311
			$lastResultsCount = 0;
312
			foreach ($rows as $row) {
313
				$wrongPath = $row['path'];
314
				$correctPath = $row['parentpath'] . '/' . $row['name'];
315
				// make sure the target is on a different subtree
316
				if (substr($correctPath, 0, strlen($wrongPath)) === $wrongPath) {
317
					// the path based parent entry is referencing one of its own children,
318
					// fix the entry's parent id instead
319
					// note: fixEntryParent cannot fail to find the parent entry by path
320
					// here because the reason we reached this code is because we already
321
					// found it
322
					$this->fixEntryParent(
323
						$out,
324
						$row['storage'],
325
						$row['fileid'],
326
						$row['path'],
327
						$row['wrongparentid'],
328
						true
329
					);
330
				} else {
331
					$this->fixEntryPath(
332
						$out,
333
						$row['fileid'],
334
						$wrongPath,
335
						$row['parentstorage'],
336
						$correctPath
337
					);
338
					// we also need to fix the target storage
339
					$affectedStorages[$row['parentstorage']] = true;
340
				}
341
				$lastResultsCount++;
342
			}
343
			$this->connection->commit();
344
345
			$totalResultsCount += $lastResultsCount;
346
347
			// note: this is not pagination but repeating the query over and over again
348
			// until all possible entries were fixed
349
		} while ($lastResultsCount > 0);
350
351
		if ($totalResultsCount > 0) {
352
			$out->info("Fixed $totalResultsCount file cache entries with wrong path");
353
		}
354
355
		return array_keys($affectedStorages);
356
	}
357
358
	/**
359
	 * Gets the file id of the entry. If none exists, create it
360
	 * up to the root if needed.
361
	 *
362
	 * @param int $storageId storage id
363
	 * @param string $path path for which to create the parent entry
364
	 * @return int file id of the newly created parent
365
	 */
366
	private function getOrCreateEntry($storageId, $path, $reuseFileId = null) {
367
		if ($path === '.') {
368
			$path = '';
369
		}
370
		// find the correct parent
371
		$qb = $this->connection->getQueryBuilder();
372
		// select fileid as "correctparentid"
373
		$qb->select('fileid')
374
			// from oc_filecache
375
			->from('filecache')
376
			// where storage=$storage and path='$parentPath'
377
			->where($qb->expr()->eq('storage', $qb->createNamedParameter($storageId)));
378
379 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...
380
			$qb->andWhere($qb->expr()->isNull('path'));
381
		} else {
382
			$qb->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter(md5($path))));
383
		}
384
		$results = $qb->execute();
385
		$rows = $results->fetchAll();
386
		$results->closeCursor();
387
388
		if (!empty($rows)) {
389
			return $rows[0]['fileid'];
390
		}
391
392
		if ($path !== '') {
393
			$parentId = $this->getOrCreateEntry($storageId, dirname($path));
394
		} else {
395
			// root entry missing, create it
396
			$parentId = -1;
397
		}
398
399
		$qb = $this->connection->getQueryBuilder();
400
		$values = [
401
			'storage' => $qb->createNamedParameter($storageId),
402
			'path' => $qb->createNamedParameter($path),
403
			'path_hash' => $qb->createNamedParameter(md5($path)),
404
			'name' => $qb->createNamedParameter(basename($path)),
405
			'parent' => $qb->createNamedParameter($parentId),
406
			'size' => $qb->createNamedParameter(-1),
407
			'etag' => $qb->createNamedParameter('zombie'),
408
			'mimetype' => $qb->createNamedParameter($this->dirMimeTypeId),
409
			'mimepart' => $qb->createNamedParameter($this->dirMimePartId),
410
		];
411
412
		if ($reuseFileId !== null) {
413
			// purpose of reusing the fileid of the parent is to salvage potential
414
			// metadata that might have previously been linked to this file id
415
			$values['fileid'] = $qb->createNamedParameter($reuseFileId);
416
		}
417
		$qb->insert('filecache')->values($values);
418
		try {
419
			$qb->execute();
420
		} catch (UniqueConstraintViolationException $e) {
421
			// This situation should no happen - need debugging information if it does
422
			\OC::$server->getLogger()->logException($e);
423
			\OC::$server->getLogger()->error("Filecache repair step tried to insert row that already existed with fileid: {$values['fileid']}");
424
			// Skip if the entry already exists
425
		}
426
427
		// If we reused the fileid then this is the id to return
428
		if($reuseFileId !== null) {
429
			// with Oracle, the trigger gets in the way and does not let us specify
430
			// a fileid value on insert
431
			if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
432
				$lastFileId = $this->connection->lastInsertId('*PREFIX*filecache');
433
				if ($reuseFileId !== $lastFileId) {
434
					// use update to set it directly
435
					$qb = $this->connection->getQueryBuilder();
436
					$qb->update('filecache')
437
						->set('fileid', $qb->createNamedParameter($reuseFileId))
438
						->where($qb->expr()->eq('fileid', $qb->createNamedParameter($lastFileId)));
439
					$qb->execute();
440
				}
441
			}
442
443
			return $reuseFileId;
444
		} else {
445
			// Else we inserted a new row with auto generated id, use that
446
			return $this->connection->lastInsertId('*PREFIX*filecache');
447
		}
448
	}
449
450
	/**
451
	 * Fixes the broken entry's path.
452
	 *
453
	 * @param IOutput $out repair output
454
	 * @param int $storageId storage id of the entry to fix
455
	 * @param int $fileId file id of the entry to fix
456
	 * @param string $path path from the entry to fix
457
	 * @param int $wrongParentId wrong parent id
458
	 * @param bool $parentIdExists true if the entry from the $wrongParentId exists (but is the wrong one),
459
	 * false if it doesn't
460
	 * @return bool true if the entry was fixed, false otherwise
461
	 */
462
	private function fixEntryParent(IOutput $out, $storageId, $fileId, $path, $wrongParentId, $parentIdExists = false) {
463
		if (!$parentIdExists) {
464
			// if the parent doesn't exist, let us reuse its id in case there is metadata to salvage
465
			$correctParentId = $this->getOrCreateEntry($storageId, dirname($path), $wrongParentId);
466
		} else {
467
			// parent exists and is the wrong one, so recreating would need a new fileid
468
			$correctParentId = $this->getOrCreateEntry($storageId, dirname($path));
469
		}
470
471
		$this->connection->beginTransaction();
472
473
		$qb = $this->connection->getQueryBuilder();
474
		$qb->update('filecache')
475
			->set('parent', $qb->createNamedParameter($correctParentId))
476
			->where($qb->expr()->eq('fileid', $qb->createNamedParameter($fileId)));
477
		$qb->execute();
478
479
		$text = "Fixed file cache entry with fileid $fileId, set wrong parent \"$wrongParentId\" to \"$correctParentId\"";
480
		$out->advance(1, $text);
481
482
		$this->connection->commit();
483
484
		return true;
485
	}
486
487
	/**
488
	 * Repair entries where the parent id doesn't point to any existing entry
489
	 * by finding the actual parent entry matching the entry's path dirname.
490
	 * 
491
	 * @param IOutput $out output
492
	 * @param int|null $storageNumericId storage to fix or null for all
493
	 * @return int number of results that were fixed
494
	 */
495
	private function fixEntriesWithNonExistingParentIdEntry(IOutput $out, $storageNumericId = null) {
496
		$qb = $this->connection->getQueryBuilder();
497
		$this->addQueryConditionsNonExistingParentIdEntry($qb, $storageNumericId);
498
		$qb->setMaxResults(self::CHUNK_SIZE);
499
500
		$totalResultsCount = 0;
501
		do {
502
			$results = $qb->execute();
503
			// since we're going to operate on fetched entry, better cache them
504
			// to avoid DB lock ups
505
			$rows = $results->fetchAll();
506
			$results->closeCursor();
507
508
			$lastResultsCount = 0;
509
			foreach ($rows as $row) {
510
				$this->fixEntryParent(
511
					$out,
512
					$row['storage'],
513
					$row['fileid'],
514
					$row['path'],
515
					$row['parent'],
516
					// in general the parent doesn't exist except
517
					// for the one condition where parent=fileid
518
					$row['parent'] === $row['fileid']
519
				);
520
				$lastResultsCount++;
521
			}
522
523
			$totalResultsCount += $lastResultsCount;
524
525
			// note: this is not pagination but repeating the query over and over again
526
			// until all possible entries were fixed
527
		} while ($lastResultsCount > 0);
528
529
		if ($totalResultsCount > 0) {
530
			$out->info("Fixed $totalResultsCount file cache entries with wrong path");
531
		}
532
533
		return $totalResultsCount;
534
	}
535
536
	/**
537
	 * Run the repair step
538
	 *
539
	 * @param IOutput $out output
540
	 */
541
	public function run(IOutput $out) {
542
543
		$this->dirMimeTypeId = $this->mimeLoader->getId('httpd/unix-directory');
544
		$this->dirMimePartId = $this->mimeLoader->getId('httpd');
545
546
		if ($this->countOnly) {
547
			$this->reportAffectedStoragesParentIdWrongPath($out);
548
			$this->reportAffectedStoragesNonExistingParentIdEntry($out);
549
		} else {
550
			$brokenPathEntries = $this->countResultsToProcessParentIdWrongPath($this->storageNumericId);
551
			$brokenParentIdEntries = $this->countResultsToProcessNonExistingParentIdEntry($this->storageNumericId);
552
			$out->startProgress($brokenPathEntries + $brokenParentIdEntries);
553
554
			$totalFixed = 0;
555
556
			/*
557
			 * This repair itself might overwrite existing target parent entries and create
558
			 * orphans where the parent entry of the parent id doesn't exist but the path matches.
559
			 * This needs to be repaired by fixEntriesWithNonExistingParentIdEntry(), this is why
560
			 * we need to keep this specific order of repair.
561
			 */
562
			$affectedStorages = $this->fixEntriesWithCorrectParentIdButWrongPath($out, $this->storageNumericId);
563
564
			if ($this->storageNumericId !== null) {
565
				foreach ($affectedStorages as $storageNumericId) {
566
					$this->fixEntriesWithNonExistingParentIdEntry($out, $storageNumericId);
567
				}
568
			} else {
569
				// just fix all
570
				$this->fixEntriesWithNonExistingParentIdEntry($out);
571
			}
572
			$out->finishProgress();
573
			$out->info('');
574
		}
575
	}
576
}
577