1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @author Bart Visscher <[email protected]> |
4
|
|
|
* @author Jörn Friedrich Dreyer <[email protected]> |
5
|
|
|
* @author [email protected] <[email protected]> |
6
|
|
|
* @author Morris Jobke <[email protected]> |
7
|
|
|
* @author Robin Appelman <[email protected]> |
8
|
|
|
* @author Thomas Müller <[email protected]> |
9
|
|
|
* @author Vincent Petry <[email protected]> |
10
|
|
|
* @author Sujith Haridasan <[email protected]> |
11
|
|
|
* |
12
|
|
|
* @copyright Copyright (c) 2018, ownCloud GmbH |
13
|
|
|
* @license AGPL-3.0 |
14
|
|
|
* |
15
|
|
|
* This code is free software: you can redistribute it and/or modify |
16
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
17
|
|
|
* as published by the Free Software Foundation. |
18
|
|
|
* |
19
|
|
|
* This program is distributed in the hope that it will be useful, |
20
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
21
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
22
|
|
|
* GNU Affero General Public License for more details. |
23
|
|
|
* |
24
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
25
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
26
|
|
|
* |
27
|
|
|
*/ |
28
|
|
|
|
29
|
|
|
namespace OCA\Files\Command; |
30
|
|
|
|
31
|
|
|
use Doctrine\DBAL\Connection; |
32
|
|
|
use OC\Core\Command\Base; |
33
|
|
|
use OC\Core\Command\InterruptedException; |
34
|
|
|
use OC\ForbiddenException; |
35
|
|
|
use OC\Migration\ConsoleOutput; |
36
|
|
|
use OC\Repair\RepairMismatchFileCachePath; |
37
|
|
|
use OCP\Files\IMimeTypeLoader; |
38
|
|
|
use OCP\Files\StorageNotAvailableException; |
39
|
|
|
use OCP\IConfig; |
40
|
|
|
use OCP\IDBConnection; |
41
|
|
|
use OCP\IGroupManager; |
42
|
|
|
use OCP\IUserManager; |
43
|
|
|
use OCP\Lock\ILockingProvider; |
44
|
|
|
use OCP\Lock\LockedException; |
45
|
|
|
use OCP\ILogger; |
46
|
|
|
use Symfony\Component\Console\Helper\Table; |
47
|
|
|
use Symfony\Component\Console\Input\InputArgument; |
48
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
49
|
|
|
use Symfony\Component\Console\Input\InputOption; |
50
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
51
|
|
|
|
52
|
|
|
class Scan extends Base { |
53
|
|
|
|
54
|
|
|
/** @var IUserManager $userManager */ |
55
|
|
|
private $userManager; |
56
|
|
|
/** @var IGroupManager $groupManager */ |
57
|
|
|
private $groupManager; |
58
|
|
|
/** @var ILockingProvider */ |
59
|
|
|
private $lockingProvider; |
60
|
|
|
/** @var IMimeTypeLoader */ |
61
|
|
|
private $mimeTypeLoader; |
62
|
|
|
/** @var ILogger */ |
63
|
|
|
private $logger; |
64
|
|
|
/** @var IConfig */ |
65
|
|
|
private $config; |
66
|
|
|
/** @var float */ |
67
|
|
|
protected $execTime = 0; |
68
|
|
|
/** @var int */ |
69
|
|
|
protected $foldersCounter = 0; |
70
|
|
|
/** @var int */ |
71
|
|
|
protected $filesCounter = 0; |
72
|
|
|
|
73
|
|
|
public function __construct( |
74
|
|
|
IUserManager $userManager, |
75
|
|
|
IGroupManager $groupManager, |
76
|
|
|
ILockingProvider $lockingProvider, |
77
|
|
|
IMimeTypeLoader $mimeTypeLoader, |
78
|
|
|
ILogger $logger, |
79
|
|
|
IConfig $config |
80
|
|
|
) { |
81
|
|
|
$this->userManager = $userManager; |
82
|
|
|
$this->groupManager = $groupManager; |
83
|
|
|
$this->lockingProvider = $lockingProvider; |
84
|
|
|
$this->mimeTypeLoader = $mimeTypeLoader; |
85
|
|
|
$this->logger = $logger; |
86
|
|
|
$this->config = $config; |
87
|
|
|
parent::__construct(); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
protected function configure() { |
91
|
|
|
parent::configure(); |
92
|
|
|
|
93
|
|
|
$this |
94
|
|
|
->setName('files:scan') |
95
|
|
|
->setDescription('Scans the filesystem for changes and updates the file cache accordingly.') |
96
|
|
|
->addArgument( |
97
|
|
|
'user_id', |
98
|
|
|
InputArgument::OPTIONAL | InputArgument::IS_ARRAY, |
99
|
|
|
'Will rescan all files of the given user(s).' |
100
|
|
|
) |
101
|
|
|
->addOption( |
102
|
|
|
'path', |
103
|
|
|
'p', |
104
|
|
|
InputArgument::OPTIONAL, |
105
|
|
|
'Limit rescan to this path, e.g., --path="/alice/files/Music", the user_id is determined by the path and the user_id parameter and --all are ignored.' |
106
|
|
|
) |
107
|
|
|
->addOption( |
108
|
|
|
'group', |
109
|
|
|
'g', |
110
|
|
|
InputOption::VALUE_IS_ARRAY|InputOption::VALUE_REQUIRED, |
111
|
|
|
'Scan user(s) under the group(s). This option can be used as --group=foo --group=bar to scan groups foo and bar' |
112
|
|
|
) |
113
|
|
|
->addOption( |
114
|
|
|
'quiet', |
115
|
|
|
'q', |
116
|
|
|
InputOption::VALUE_NONE, |
117
|
|
|
'Suppress any output.' |
118
|
|
|
) |
119
|
|
|
->addOption( |
120
|
|
|
'verbose', |
121
|
|
|
'-v|vv|vvv', |
122
|
|
|
InputOption::VALUE_NONE, |
123
|
|
|
"Increase the output's verbosity." |
124
|
|
|
) |
125
|
|
|
->addOption( |
126
|
|
|
'all', |
127
|
|
|
null, |
128
|
|
|
InputOption::VALUE_NONE, |
129
|
|
|
'Will rescan all files of all known users.' |
130
|
|
|
) |
131
|
|
|
->addOption( |
132
|
|
|
'repair', |
133
|
|
|
null, |
134
|
|
|
InputOption::VALUE_NONE, |
135
|
|
|
'Will repair detached filecache entries (slow).' |
136
|
|
|
)->addOption( |
137
|
|
|
'unscanned', |
138
|
|
|
null, |
139
|
|
|
InputOption::VALUE_NONE, |
140
|
|
|
'Only scan files which are marked as not fully scanned.' |
141
|
|
|
); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
public function checkScanWarning($fullPath, OutputInterface $output) { |
145
|
|
|
$normalizedPath = \basename(\OC\Files\Filesystem::normalizePath($fullPath)); |
146
|
|
|
$path = \basename($fullPath); |
147
|
|
|
|
148
|
|
|
if ($normalizedPath !== $path) { |
149
|
|
|
$output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>'); |
150
|
|
|
} |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* Repair all storages at once |
155
|
|
|
* |
156
|
|
|
* @param OutputInterface $output |
157
|
|
|
*/ |
158
|
|
|
protected function repairAll(OutputInterface $output) { |
159
|
|
|
$connection = $this->reconnectToDatabase($output); |
160
|
|
|
$repairStep = new RepairMismatchFileCachePath( |
161
|
|
|
$connection, |
162
|
|
|
$this->mimeTypeLoader, |
163
|
|
|
$this->logger, |
164
|
|
|
$this->config |
165
|
|
|
); |
166
|
|
|
$repairStep->setStorageNumericId(null); |
167
|
|
|
$repairStep->setCountOnly(false); |
168
|
|
|
$repairStep->doRepair(new ConsoleOutput($output)); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
protected function scanFiles($user, $path, $verbose, OutputInterface $output, $backgroundScan = false, $shouldRepair = false) { |
172
|
|
|
$connection = $this->reconnectToDatabase($output); |
173
|
|
|
$scanner = new \OC\Files\Utils\Scanner($user, $connection, \OC::$server->getLogger()); |
174
|
|
|
if ($shouldRepair) { |
175
|
|
|
$scanner->listen('\OC\Files\Utils\Scanner', 'beforeScanStorage', function ($storage) use ($output, $connection) { |
176
|
|
|
try { |
177
|
|
|
// FIXME: this will lock the storage even if there is nothing to repair |
178
|
|
|
$storage->acquireLock('', ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider); |
179
|
|
|
} catch (LockedException $e) { |
180
|
|
|
$output->writeln("\t<error>Storage \"" . $storage->getCache()->getNumericStorageId() . '" cannot be repaired as it is currently in use, please try again later</error>'); |
181
|
|
|
return; |
182
|
|
|
} |
183
|
|
|
try { |
184
|
|
|
$repairStep = new RepairMismatchFileCachePath( |
185
|
|
|
$connection, |
186
|
|
|
$this->mimeTypeLoader, |
187
|
|
|
$this->logger, |
188
|
|
|
$this->config |
189
|
|
|
); |
190
|
|
|
$repairStep->setStorageNumericId($storage->getCache()->getNumericStorageId()); |
191
|
|
|
$repairStep->setCountOnly(false); |
192
|
|
|
$repairStep->run(new ConsoleOutput($output)); |
193
|
|
|
} finally { |
194
|
|
|
$storage->releaseLock('', ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider); |
195
|
|
|
} |
196
|
|
|
}); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception |
200
|
|
|
# printout and count |
201
|
|
|
if ($verbose) { |
202
|
|
|
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) { |
203
|
|
|
$output->writeln("\tFile <info>$path</info>"); |
204
|
|
|
$this->filesCounter += 1; |
205
|
|
|
if ($this->hasBeenInterrupted()) { |
206
|
|
|
throw new InterruptedException(); |
207
|
|
|
} |
208
|
|
|
}); |
209
|
|
|
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) { |
210
|
|
|
$output->writeln("\tFolder <info>$path</info>"); |
211
|
|
|
$this->foldersCounter += 1; |
212
|
|
|
if ($this->hasBeenInterrupted()) { |
213
|
|
|
throw new InterruptedException(); |
214
|
|
|
} |
215
|
|
|
}); |
216
|
|
|
$scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) { |
217
|
|
|
$output->writeln("Error while scanning, storage not available (" . $e->getMessage() . ")"); |
218
|
|
|
}); |
219
|
|
|
# count only |
220
|
|
|
} else { |
221
|
|
|
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function () { |
222
|
|
|
$this->filesCounter += 1; |
223
|
|
|
if ($this->hasBeenInterrupted()) { |
224
|
|
|
throw new InterruptedException(); |
225
|
|
|
} |
226
|
|
|
}); |
227
|
|
|
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function () { |
228
|
|
|
$this->foldersCounter += 1; |
229
|
|
|
if ($this->hasBeenInterrupted()) { |
230
|
|
|
throw new InterruptedException(); |
231
|
|
|
} |
232
|
|
|
}); |
233
|
|
|
} |
234
|
|
|
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) { |
235
|
|
|
$this->checkScanWarning($path, $output); |
236
|
|
|
}); |
237
|
|
|
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) { |
238
|
|
|
$this->checkScanWarning($path, $output); |
239
|
|
|
}); |
240
|
|
|
|
241
|
|
|
try { |
242
|
|
|
if ($backgroundScan) { |
243
|
|
|
$scanner->backgroundScan($path); |
244
|
|
|
} else { |
245
|
|
|
$scanner->scan($path, $shouldRepair); |
|
|
|
|
246
|
|
|
} |
247
|
|
|
} catch (ForbiddenException $e) { |
248
|
|
|
$output->writeln("<error>Home storage for user $user not writable</error>"); |
249
|
|
|
$output->writeln("Make sure you're running the scan command only as the user the web server runs as"); |
250
|
|
|
} catch (InterruptedException $e) { |
251
|
|
|
# exit the function if ctrl-c has been pressed |
252
|
|
|
$output->writeln('Interrupted by user'); |
253
|
|
|
return; |
254
|
|
|
} catch (\Exception $e) { |
255
|
|
|
$output->writeln('<error>Exception during scan: ' . \get_class($e) . ': ' . $e->getMessage() . "\n" . $e->getTraceAsString() . '</error>'); |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
protected function getAllUsersFromGroup($group) { |
260
|
|
|
$count = 0; |
261
|
|
|
$users = []; |
262
|
|
|
foreach ($this->groupManager->findUsersInGroup($group) as $user) { |
263
|
|
|
\array_push($users, $user->getUID()); |
264
|
|
|
$count++; |
265
|
|
|
//Take 200 users at a time |
266
|
|
|
if ($count > 199) { |
267
|
|
|
yield $users; |
268
|
|
|
$count = 1; |
269
|
|
|
$users = []; |
270
|
|
|
} |
271
|
|
|
} |
272
|
|
|
if (\count($users) > 0) { |
273
|
|
|
yield $users; |
274
|
|
|
} |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) { |
278
|
|
|
$inputPath = $input->getOption('path'); |
279
|
|
|
$groups = $input->getOption('group'); |
280
|
|
|
$shouldRepairStoragesIndividually = (bool) $input->getOption('repair'); |
281
|
|
|
|
282
|
|
|
if (\count($groups) >= 1) { |
283
|
|
|
$users = []; |
284
|
|
|
foreach ($groups as $group) { |
|
|
|
|
285
|
|
|
if ($this->groupManager->groupExists($group) === false) { |
286
|
|
|
$output->writeln("Group name $group doesn't exist"); |
287
|
|
|
return 1; |
288
|
|
|
} else { |
289
|
|
|
$users[$group] = []; |
290
|
|
|
foreach ($this->getAllUsersFromGroup($group) as $users_array) { |
291
|
|
|
$users[$group] = $users_array; |
292
|
|
|
$this->processUserChunks($input, $output, $users, $inputPath, $shouldRepairStoragesIndividually, $group); |
293
|
|
|
} |
294
|
|
|
} |
295
|
|
|
} |
296
|
|
|
} elseif ($inputPath) { |
297
|
|
|
$inputPath = '/' . \trim($inputPath, '/'); |
298
|
|
|
list(, $user, ) = \explode('/', $inputPath, 3); |
299
|
|
|
$users = [$user]; |
300
|
|
|
} elseif ($input->getOption('all')) { |
301
|
|
|
// we can only repair all storages in bulk (more efficient) if singleuser or maintenance mode |
302
|
|
|
// is enabled to prevent concurrent user access |
303
|
|
|
if ($input->getOption('repair')) { |
304
|
|
|
if ($this->config->getSystemValue('singleuser', false) || $this->config->getSystemValue('maintenance', false)) { |
305
|
|
|
// repair all storages at once |
306
|
|
|
$this->repairAll($output); |
307
|
|
|
// don't fix individually |
308
|
|
|
$shouldRepairStoragesIndividually = false; |
309
|
|
|
} else { |
310
|
|
|
$output->writeln("<comment>Please switch to single user mode to repair all storages: occ maintenance:singleuser --on</comment>"); |
311
|
|
|
$output->writeln("<comment>Alternatively, you can specify a user to repair. Please note that this is slower than repairing in bulk</comment>"); |
312
|
|
|
return 1; |
313
|
|
|
} |
314
|
|
|
} |
315
|
|
|
$users = $this->userManager->search(''); |
316
|
|
|
} else { |
317
|
|
|
$users = $input->getArgument('user_id'); |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
if (\count($groups) === 0) { |
321
|
|
|
$this->processUserChunks($input, $output, $users, $inputPath, $shouldRepairStoragesIndividually); |
322
|
|
|
} |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
protected function processUserChunks($input, $output, $users, $inputPath, $shouldRepairStoragesIndividually, $group = null) { |
326
|
|
|
# no messaging level option means: no full printout but statistics |
327
|
|
|
# $quiet means no print at all |
328
|
|
|
# $verbose means full printout including statistics |
329
|
|
|
# -q -v full stat |
330
|
|
|
# 0 0 no yes |
331
|
|
|
# 0 1 yes yes |
332
|
|
|
# 1 -- no no (quiet overrules verbose) |
333
|
|
|
$verbose = $input->getOption('verbose'); |
334
|
|
|
$quiet = $input->getOption('quiet'); |
335
|
|
|
# restrict the verbosity level to VERBOSITY_VERBOSE |
336
|
|
|
if ($output->getVerbosity()>OutputInterface::VERBOSITY_VERBOSE) { |
337
|
|
|
$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); |
338
|
|
|
} |
339
|
|
|
if ($quiet) { |
340
|
|
|
$verbose = false; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
# check quantity of users to be process and show it on the command line |
344
|
|
|
$users_total = \count($users); |
345
|
|
|
if ($users_total === 0) { |
346
|
|
|
$output->writeln("<error>Please specify the user id to scan, \"--all\" to scan for all users or \"--path=...\"</error>"); |
347
|
|
|
return; |
348
|
|
|
} else { |
349
|
|
|
$this->initTools(); |
350
|
|
|
if ($group !== null) { |
351
|
|
|
$output->writeln("Scanning group $group"); |
352
|
|
|
$this->userScan($users[$group], $inputPath, $shouldRepairStoragesIndividually, $input, $output, $verbose); |
353
|
|
|
} elseif ($users_total >= 1) { |
354
|
|
|
$output->writeln("\nScanning files for $users_total users"); |
355
|
|
|
$this->userScan($users, $inputPath, $shouldRepairStoragesIndividually, $input, $output, $verbose); |
356
|
|
|
} |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
# stat: printout statistics if $quiet was not set |
360
|
|
|
if (!$quiet) { |
361
|
|
|
$this->presentStats($output); |
362
|
|
|
} |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
/** |
366
|
|
|
* Initialises some useful tools for the Command |
367
|
|
|
*/ |
368
|
|
|
protected function initTools() { |
369
|
|
|
// Start the timer |
370
|
|
|
$this->execTime = -\microtime(true); |
|
|
|
|
371
|
|
|
// Convert PHP errors to exceptions |
372
|
|
|
\set_error_handler([$this, 'exceptionErrorHandler'], E_ALL); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
protected function userScan($users, $inputPath, $shouldRepairStoragesIndividually, $input, $output, $verbose) { |
376
|
|
|
$users_total = \count($users); |
377
|
|
|
$user_count = 0; |
378
|
|
|
foreach ($users as $user) { |
379
|
|
|
if (\is_object($user)) { |
380
|
|
|
$user = $user->getUID(); |
381
|
|
|
} |
382
|
|
|
$path = $inputPath ? $inputPath : '/' . $user; |
383
|
|
|
$user_count += 1; |
384
|
|
|
if ($this->userManager->userExists($user)) { |
385
|
|
|
# add an extra line when verbose is set to optical separate users |
386
|
|
|
if ($verbose) { |
387
|
|
|
$output->writeln(""); |
388
|
|
|
} |
389
|
|
|
$r = $shouldRepairStoragesIndividually ? ' (and repair)' : ''; |
390
|
|
|
$output->writeln("Starting scan$r for user $user_count out of $users_total ($user)"); |
391
|
|
|
# full: printout data if $verbose was set |
392
|
|
|
$this->scanFiles($user, $path, $verbose, $output, $input->getOption('unscanned'), $shouldRepairStoragesIndividually); |
393
|
|
|
} else { |
394
|
|
|
$output->writeln("<error>Unknown user $user_count $user</error>"); |
395
|
|
|
} |
396
|
|
|
# check on each user if there was a user interrupt (ctrl-c) and exit foreach |
397
|
|
|
if ($this->hasBeenInterrupted()) { |
398
|
|
|
break; |
399
|
|
|
} |
400
|
|
|
} |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* Processes PHP errors as exceptions in order to be able to keep track of problems |
405
|
|
|
* |
406
|
|
|
* @see https://secure.php.net/manual/en/function.set-error-handler.php |
407
|
|
|
* |
408
|
|
|
* @param int $severity the level of the error raised |
409
|
|
|
* @param string $message |
410
|
|
|
* @param string $file the filename that the error was raised in |
411
|
|
|
* @param int $line the line number the error was raised |
412
|
|
|
* |
413
|
|
|
* @throws \ErrorException |
414
|
|
|
*/ |
415
|
|
|
public function exceptionErrorHandler($severity, $message, $file, $line) { |
416
|
|
|
if (!(\error_reporting() & $severity)) { |
417
|
|
|
// This error code is not included in error_reporting |
418
|
|
|
return; |
419
|
|
|
} |
420
|
|
|
throw new \ErrorException($message, 0, $severity, $file, $line); |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
/** |
424
|
|
|
* @param OutputInterface $output |
425
|
|
|
*/ |
426
|
|
|
protected function presentStats(OutputInterface $output) { |
427
|
|
|
// Stop the timer |
428
|
|
|
$this->execTime += \microtime(true); |
429
|
|
|
$output->writeln(""); |
430
|
|
|
|
431
|
|
|
$headers = [ |
432
|
|
|
'Folders', 'Files', 'Elapsed time', 'Items per second' |
433
|
|
|
]; |
434
|
|
|
|
435
|
|
|
$this->showSummary($headers, null, $output); |
|
|
|
|
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
/** |
439
|
|
|
* Shows a summary of operations |
440
|
|
|
* |
441
|
|
|
* @param string[] $headers |
442
|
|
|
* @param string[] $rows |
443
|
|
|
* @param OutputInterface $output |
444
|
|
|
*/ |
445
|
|
|
protected function showSummary($headers, $rows, OutputInterface $output) { |
446
|
|
|
$niceDate = $this->formatExecTime(); |
447
|
|
|
$itemsPerSecond = $this->getItemsPerSecond(); |
448
|
|
|
if (!$rows) { |
|
|
|
|
449
|
|
|
$rows = [ |
450
|
|
|
$this->foldersCounter, |
451
|
|
|
$this->filesCounter, |
452
|
|
|
$niceDate, |
453
|
|
|
$itemsPerSecond |
454
|
|
|
]; |
455
|
|
|
} |
456
|
|
|
$table = new Table($output); |
457
|
|
|
$table |
458
|
|
|
->setHeaders($headers) |
459
|
|
|
->setRows([$rows]); |
460
|
|
|
$table->render(); |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
/** |
464
|
|
|
* Get items per second processed, no fractions |
465
|
|
|
* |
466
|
|
|
* @return string |
467
|
|
|
*/ |
468
|
|
|
protected function getItemsPerSecond() { |
469
|
|
|
$items = $this->foldersCounter + $this->filesCounter; |
470
|
|
|
if ($this->execTime === 0) { |
471
|
|
|
// catch div by 0 |
472
|
|
|
$itemsPerSecond = 0; |
473
|
|
|
} else { |
474
|
|
|
$itemsPerSecond = $items / $this->execTime; |
475
|
|
|
} |
476
|
|
|
return \sprintf("%.0f", $itemsPerSecond); |
477
|
|
|
} |
478
|
|
|
|
479
|
|
|
/** |
480
|
|
|
* Formats microtime into a human readable format |
481
|
|
|
* |
482
|
|
|
* @return string |
483
|
|
|
*/ |
484
|
|
|
protected function formatExecTime() { |
485
|
|
|
list($secs, $tens) = \explode('.', \sprintf("%.1f", ($this->execTime))); |
|
|
|
|
486
|
|
|
|
487
|
|
|
# if you want to have microseconds add this: . '.' . $tens; |
488
|
|
|
return \date('H:i:s', $secs); |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
/** |
492
|
|
|
* @return \OCP\IDBConnection |
493
|
|
|
*/ |
494
|
|
|
protected function reconnectToDatabase(OutputInterface $output) { |
495
|
|
|
/** @var Connection | IDBConnection $connection*/ |
496
|
|
|
$connection = \OC::$server->getDatabaseConnection(); |
497
|
|
|
try { |
498
|
|
|
$connection->close(); |
499
|
|
|
} catch (\Exception $ex) { |
500
|
|
|
$output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>"); |
501
|
|
|
} |
502
|
|
|
while (!$connection->isConnected()) { |
|
|
|
|
503
|
|
|
try { |
504
|
|
|
$connection->connect(); |
505
|
|
|
} catch (\Exception $ex) { |
506
|
|
|
$output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>"); |
507
|
|
|
\sleep(60); |
508
|
|
|
} |
509
|
|
|
} |
510
|
|
|
return $connection; |
511
|
|
|
} |
512
|
|
|
} |
513
|
|
|
|
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.