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) { |
|
|
|
|
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); |
|
|
|
|
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') |
|
|
|
|
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); |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.