Completed
Push — master ( 6994a2...1178e8 )
by Roeland
13:08
created

Scan::showSummary()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15

Duplication

Lines 15
Ratio 100 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 3
dl 15
loc 15
rs 9.7666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bart Visscher <[email protected]>
6
 * @author Jörn Friedrich Dreyer <[email protected]>
7
 * @author [email protected] <[email protected]>
8
 * @author Morris Jobke <[email protected]>
9
 * @author Robin Appelman <[email protected]>
10
 * @author Thomas Müller <[email protected]>
11
 * @author Vincent Petry <[email protected]>
12
 *
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 OCP\Files\Mount\IMountPoint;
36
use OCP\Files\NotFoundException;
37
use OCP\Files\StorageNotAvailableException;
38
use OCP\IDBConnection;
39
use OCP\IUserManager;
40
use Symfony\Component\Console\Input\InputArgument;
41
use Symfony\Component\Console\Input\InputInterface;
42
use Symfony\Component\Console\Input\InputOption;
43
use Symfony\Component\Console\Output\OutputInterface;
44
use Symfony\Component\Console\Helper\Table;
45
46
class Scan extends Base {
47
48
	/** @var IUserManager $userManager */
49
	private $userManager;
50
	/** @var float */
51
	protected $execTime = 0;
52
	/** @var int */
53
	protected $foldersCounter = 0;
54
	/** @var int */
55
	protected $filesCounter = 0;
56
57
	public function __construct(IUserManager $userManager) {
58
		$this->userManager = $userManager;
59
		parent::__construct();
60
	}
61
62
	protected function configure() {
63
		parent::configure();
64
65
		$this
66
			->setName('files:scan')
67
			->setDescription('rescan filesystem')
68
			->addArgument(
69
				'user_id',
70
				InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
71
				'will rescan all files of the given user(s)'
72
			)
73
			->addOption(
74
				'path',
75
				'p',
76
				InputArgument::OPTIONAL,
77
				'limit rescan to this path, eg. --path="/alice/files/Music", the user_id is determined by the path and the user_id parameter and --all are ignored'
78
			)
79
			->addOption(
80
				'quiet',
81
				'q',
82
				InputOption::VALUE_NONE,
83
				'suppress any output'
84
			)
85
			->addOption(
86
				'verbose',
87
				'-v|vv|vvv',
88
				InputOption::VALUE_NONE,
89
				'verbose the output'
90
			)
91
			->addOption(
92
				'all',
93
				null,
94
				InputOption::VALUE_NONE,
95
				'will rescan all files of all known users'
96
			)->addOption(
97
				'unscanned',
98
				null,
99
				InputOption::VALUE_NONE,
100
				'only scan files which are marked as not fully scanned'
101
			)->addOption(
102
				'shallow',
103
				null,
104
				InputOption::VALUE_NONE,
105
				'do not scan folders recursively'
106
			)->addOption(
107
				'home-only',
108
				null,
109
				InputOption::VALUE_NONE,
110
				'only scan the home storage, ignoring any mounted external storage or share'
111
			);
112
	}
113
114 View Code Duplication
	public function checkScanWarning($fullPath, OutputInterface $output) {
115
		$normalizedPath = basename(\OC\Files\Filesystem::normalizePath($fullPath));
116
		$path = basename($fullPath);
117
118
		if ($normalizedPath !== $path) {
119
			$output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>');
120
		}
121
	}
122
123
	protected function scanFiles($user, $path, $verbose, OutputInterface $output, $backgroundScan = false, $recursive = true, $homeOnly = false) {
124
		$connection = $this->reconnectToDatabase($output);
125
		$scanner = new \OC\Files\Utils\Scanner($user, $connection, \OC::$server->getLogger());
126
		# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
127
		# printout and count
128
		if ($verbose) {
129
			$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) {
130
				$output->writeln("\tFile   <info>$path</info>");
131
				$this->filesCounter += 1;
132
				$this->abortIfInterrupted();
133
			});
134
			$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
135
				$output->writeln("\tFolder <info>$path</info>");
136
				$this->foldersCounter += 1;
137
				$this->abortIfInterrupted();
138
			});
139
			$scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) {
140
				$output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')');
141
			});
142
			# count only
143
		} else {
144
			$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function () use ($output) {
145
				$this->filesCounter += 1;
146
				$this->abortIfInterrupted();
147
			});
148
			$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function () use ($output) {
149
				$this->foldersCounter += 1;
150
				$this->abortIfInterrupted();
151
			});
152
		}
153
		$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) {
154
			$this->checkScanWarning($path, $output);
155
		});
156
		$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
157
			$this->checkScanWarning($path, $output);
158
		});
159
160
		try {
161
			if ($backgroundScan) {
162
				$scanner->backgroundScan($path);
163
			} else {
164
				$scanner->scan($path, $recursive, $homeOnly ? [$this, 'filterHomeMount'] : null);
165
			}
166
		} catch (ForbiddenException $e) {
167
			$output->writeln("<error>Home storage for user $user not writable</error>");
168
			$output->writeln('Make sure you\'re running the scan command only as the user the web server runs as');
169
		} catch (InterruptedException $e) {
170
			# exit the function if ctrl-c has been pressed
171
			$output->writeln('Interrupted by user');
172
		} catch (NotFoundException $e) {
173
			$output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>');
174
		} catch (\Exception $e) {
175
			$output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>');
176
			$output->writeln('<error>' . $e->getTraceAsString() . '</error>');
177
		}
178
	}
179
180
	public function filterHomeMount(IMountPoint $mountPoint) {
181
		// any mountpoint inside '/$user/files/'
182
		return substr_count($mountPoint->getMountPoint(), '/') <= 3;
183
	}
184
185
	protected function execute(InputInterface $input, OutputInterface $output) {
186
		$inputPath = $input->getOption('path');
187
		if ($inputPath) {
188
			$inputPath = '/' . trim($inputPath, '/');
189
			list (, $user,) = explode('/', $inputPath, 3);
190
			$users = array($user);
191
		} else if ($input->getOption('all')) {
192
			$users = $this->userManager->search('');
193
		} else {
194
			$users = $input->getArgument('user_id');
195
		}
196
197
		# no messaging level option means: no full printout but statistics
198
		# $quiet   means no print at all
199
		# $verbose means full printout including statistics
200
		# -q	-v	full	stat
201
		#  0	 0	no	yes
202
		#  0	 1	yes	yes
203
		#  1	--	no	no  (quiet overrules verbose)
204
		$verbose = $input->getOption('verbose');
205
		$quiet = $input->getOption('quiet');
206
		# restrict the verbosity level to VERBOSITY_VERBOSE
207
		if ($output->getVerbosity() > OutputInterface::VERBOSITY_VERBOSE) {
208
			$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
209
		}
210
		if ($quiet) {
211
			$verbose = false;
212
		}
213
214
		# check quantity of users to be process and show it on the command line
215
		$users_total = count($users);
216
		if ($users_total === 0) {
217
			$output->writeln("<error>Please specify the user id to scan, \"--all\" to scan for all users or \"--path=...\"</error>");
218
			return;
219
		} else {
220
			if ($users_total > 1) {
221
				$output->writeln("\nScanning files for $users_total users");
222
			}
223
		}
224
225
		$this->initTools();
226
227
		$user_count = 0;
228
		foreach ($users as $user) {
229
			if (is_object($user)) {
230
				$user = $user->getUID();
231
			}
232
			$path = $inputPath ? $inputPath : '/' . $user;
233
			$user_count += 1;
234
			if ($this->userManager->userExists($user)) {
235
				# add an extra line when verbose is set to optical separate users
236
				if ($verbose) {
237
					$output->writeln("");
238
				}
239
				$output->writeln("Starting scan for user $user_count out of $users_total ($user)");
240
				# full: printout data if $verbose was set
241
				$this->scanFiles($user, $path, $verbose, $output, $input->getOption('unscanned'), ! $input->getOption('shallow'), $input->getOption('home-only'));
242
			} else {
243
				$output->writeln("<error>Unknown user $user_count $user</error>");
244
			}
245
246
			try {
247
				$this->abortIfInterrupted();
248
			} catch(InterruptedException $e) {
249
				break;
250
			}
251
		}
252
253
		# stat: printout statistics if $quiet was not set
254
		if (!$quiet) {
255
			$this->presentStats($output);
256
		}
257
	}
258
259
	/**
260
	 * Initialises some useful tools for the Command
261
	 */
262
	protected function initTools() {
263
		// Start the timer
264
		$this->execTime = -microtime(true);
0 ignored issues
show
Documentation Bug introduced by
The property $execTime was declared of type double, but -microtime(true) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
265
		// Convert PHP errors to exceptions
266
		set_error_handler([$this, 'exceptionErrorHandler'], E_ALL);
267
	}
268
269
	/**
270
	 * Processes PHP errors as exceptions in order to be able to keep track of problems
271
	 *
272
	 * @see https://secure.php.net/manual/en/function.set-error-handler.php
273
	 *
274
	 * @param int $severity the level of the error raised
275
	 * @param string $message
276
	 * @param string $file the filename that the error was raised in
277
	 * @param int $line the line number the error was raised
278
	 *
279
	 * @throws \ErrorException
280
	 */
281
	public function exceptionErrorHandler($severity, $message, $file, $line) {
282
		if (!(error_reporting() & $severity)) {
283
			// This error code is not included in error_reporting
284
			return;
285
		}
286
		throw new \ErrorException($message, 0, $severity, $file, $line);
287
	}
288
289
	/**
290
	 * @param OutputInterface $output
291
	 */
292 View Code Duplication
	protected function presentStats(OutputInterface $output) {
293
		// Stop the timer
294
		$this->execTime += microtime(true);
295
		$output->writeln("");
296
297
		$headers = [
298
			'Folders', 'Files', 'Elapsed time'
299
		];
300
301
		$this->showSummary($headers, null, $output);
302
	}
303
304
	/**
305
	 * Shows a summary of operations
306
	 *
307
	 * @param string[] $headers
308
	 * @param string[] $rows
309
	 * @param OutputInterface $output
310
	 */
311 View Code Duplication
	protected function showSummary($headers, $rows, OutputInterface $output) {
312
		$niceDate = $this->formatExecTime();
313
		if (!$rows) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rows of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
314
			$rows = [
315
				$this->foldersCounter,
316
				$this->filesCounter,
317
				$niceDate,
318
			];
319
		}
320
		$table = new Table($output);
321
		$table
322
			->setHeaders($headers)
323
			->setRows([$rows]);
324
		$table->render();
325
	}
326
327
328
	/**
329
	 * Formats microtime into a human readable format
330
	 *
331
	 * @return string
332
	 */
333
	protected function formatExecTime() {
334
		list($secs, ) = explode('.', sprintf("%.1f", $this->execTime));
335
336
		# if you want to have microseconds add this:   . '.' . $tens;
337
		return date('H:i:s', $secs);
338
	}
339
340
	/**
341
	 * @return \OCP\IDBConnection
342
	 */
343 View Code Duplication
	protected function reconnectToDatabase(OutputInterface $output) {
344
		/** @var Connection | IDBConnection $connection */
345
		$connection = \OC::$server->getDatabaseConnection();
346
		try {
347
			$connection->close();
348
		} catch (\Exception $ex) {
349
			$output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
350
		}
351
		while (!$connection->isConnected()) {
0 ignored issues
show
Bug introduced by
The method isConnected() does not exist on OCP\IDBConnection. Did you maybe mean connect()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
352
			try {
353
				$connection->connect();
354
			} catch (\Exception $ex) {
355
				$output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
356
				sleep(60);
357
			}
358
		}
359
		return $connection;
360
	}
361
362
}
363