Completed
Push — master ( a32311...6b12f9 )
by Morris
09:16
created

Scan::checkScanWarning()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 8
rs 9.4285
cc 2
eloc 5
nc 2
nop 2
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
 *
10
 * @copyright Copyright (c) 2016, ownCloud, Inc.
11
 * @license AGPL-3.0
12
 *
13
 * This code is free software: you can redistribute it and/or modify
14
 * it under the terms of the GNU Affero General Public License, version 3,
15
 * as published by the Free Software Foundation.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License, version 3,
23
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
24
 *
25
 */
26
27
namespace OCA\Files\Command;
28
29
use OC\Core\Command\Base;
30
use OC\ForbiddenException;
31
use OCP\Files\StorageNotAvailableException;
32
use OCP\IUserManager;
33
use Symfony\Component\Console\Input\InputArgument;
34
use Symfony\Component\Console\Input\InputInterface;
35
use Symfony\Component\Console\Input\InputOption;
36
use Symfony\Component\Console\Output\OutputInterface;
37
use Symfony\Component\Console\Helper\Table;
38
39
class Scan extends Base {
40
41
	/** @var IUserManager $userManager */
42
	private $userManager;
43
	/** @var float */
44
	protected $execTime = 0;
45
	/** @var int */
46
	protected $foldersCounter = 0;
47
	/** @var int */
48
	protected $filesCounter = 0;
49
50
	public function __construct(IUserManager $userManager) {
51
		$this->userManager = $userManager;
52
		parent::__construct();
53
	}
54
55
	protected function configure() {
56
		parent::configure();
57
58
		$this
59
			->setName('files:scan')
60
			->setDescription('rescan filesystem')
61
			->addArgument(
62
				'user_id',
63
				InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
64
				'will rescan all files of the given user(s)'
65
			)
66
			->addOption(
67
				'path',
68
				'p',
69
				InputArgument::OPTIONAL,
70
				'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'
71
			)
72
			->addOption(
73
				'quiet',
74
				'q',
75
				InputOption::VALUE_NONE,
76
				'suppress any output'
77
			)
78
			->addOption(
79
				'verbose',
80
				'-v|vv|vvv',
81
				InputOption::VALUE_NONE,
82
				'verbose the output'
83
			)
84
			->addOption(
85
				'all',
86
				null,
87
				InputOption::VALUE_NONE,
88
				'will rescan all files of all known users'
89
			);
90
	}
91
92
	public function checkScanWarning($fullPath, OutputInterface $output) {
93
		$normalizedPath = basename(\OC\Files\Filesystem::normalizePath($fullPath));
94
		$path = basename($fullPath);
95
96
		if ($normalizedPath !== $path) {
97
			$output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>');
98
		}
99
	}
100
101
	protected function scanFiles($user, $path, $verbose, OutputInterface $output) {
102
		$scanner = new \OC\Files\Utils\Scanner($user, \OC::$server->getDatabaseConnection(), \OC::$server->getLogger());
103
		# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
104
		# printout and count
105
		if ($verbose) {
106
			$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) {
107
				$output->writeln("\tFile   <info>$path</info>");
108
				$this->filesCounter += 1;
109
				if ($this->hasBeenInterrupted()) {
110
					throw new \Exception('ctrl-c');
111
				}
112
			});
113
			$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
114
				$output->writeln("\tFolder <info>$path</info>");
115
				$this->foldersCounter += 1;
116
				if ($this->hasBeenInterrupted()) {
117
					throw new \Exception('ctrl-c');
118
				}
119
			});
120
			$scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) {
121
				$output->writeln("Error while scanning, storage not available (" . $e->getMessage() . ")");
122
			});
123
		# count only
124
		} else {
125
			$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function () use ($output) {
126
				$this->filesCounter += 1;
127
				if ($this->hasBeenInterrupted()) {
128
					throw new \Exception('ctrl-c');
129
				}
130
			});
131
			$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function () use ($output) {
132
				$this->foldersCounter += 1;
133
				if ($this->hasBeenInterrupted()) {
134
					throw new \Exception('ctrl-c');
135
				}
136
			});
137
		}
138
		$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function($path) use ($output) {
139
			$this->checkScanWarning($path, $output);
140
		});
141
		$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function($path) use ($output) {
142
			$this->checkScanWarning($path, $output);
143
		});
144
145
		try {
146
			$scanner->scan($path);
147
		} catch (ForbiddenException $e) {
148
			$output->writeln("<error>Home storage for user $user not writable</error>");
149
			$output->writeln("Make sure you're running the scan command only as the user the web server runs as");
150
		} catch (\Exception $e) {
151
			if ($e->getMessage() !== 'ctrl-c') {
152
				$output->writeln('<error>Exception while scanning: ' . $e->getMessage() . "\n" . $e->getTraceAsString() . '</error>');
153
			}
154
			return;
155
		}
156
	}
157
158
159
	protected function execute(InputInterface $input, OutputInterface $output) {
160
		$inputPath = $input->getOption('path');
161
		if ($inputPath) {
162
			$inputPath = '/' . trim($inputPath, '/');
163
			list (, $user,) = explode('/', $inputPath, 3);
164
			$users = array($user);
165
		} else if ($input->getOption('all')) {
166
			$users = $this->userManager->search('');
167
		} else {
168
			$users = $input->getArgument('user_id');
169
		}
170
171
		# no messaging level option means: no full printout but statistics
172
		# $quiet   means no print at all
173
		# $verbose means full printout including statistics
174
		# -q	-v	full	stat
175
		#  0	 0	no	yes
176
		#  0	 1	yes	yes
177
		#  1	--	no	no  (quiet overrules verbose)
178
		$verbose = $input->getOption('verbose');
179
		$quiet = $input->getOption('quiet');
180
		# restrict the verbosity level to VERBOSITY_VERBOSE
181
		if ($output->getVerbosity()>OutputInterface::VERBOSITY_VERBOSE) {
182
			$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
183
		}
184
		if ($quiet) {
185
			$verbose = false;
186
		}
187
188
		# check quantity of users to be process and show it on the command line
189
		$users_total = count($users);
190
		if ($users_total === 0) {
191
			$output->writeln("<error>Please specify the user id to scan, \"--all\" to scan for all users or \"--path=...\"</error>");
192
			return;
193
		} else {
194
			if ($users_total > 1) {
195
				$output->writeln("\nScanning files for $users_total users");
196
			}
197
		}
198
199
		$this->initTools();
200
201
		$user_count = 0;
202
		foreach ($users as $user) {
203
			if (is_object($user)) {
204
				$user = $user->getUID();
205
			}
206
			$path = $inputPath ? $inputPath : '/' . $user;
207
			$user_count += 1;
208
			if ($this->userManager->userExists($user)) {
209
				# add an extra line when verbose is set to optical separate users
210
				if ($verbose) {$output->writeln(""); }
211
				$output->writeln("Starting scan for user $user_count out of $users_total ($user)");
212
				# full: printout data if $verbose was set
213
				$this->scanFiles($user, $path, $verbose, $output);
214
			} else {
215
				$output->writeln("<error>Unknown user $user_count $user</error>");
216
			}
217
			# check on each user if there was a user interrupt (ctrl-c) and exit foreach
218
			if ($this->hasBeenInterrupted()) {
219
				break;
220
			}
221
		}
222
223
		# stat: printout statistics if $quiet was not set
224
		if (!$quiet) {
225
			$this->presentStats($output);
226
		}
227
	}
228
229
	/**
230
	 * Initialises some useful tools for the Command
231
	 */
232
	protected function initTools() {
233
		// Start the timer
234
		$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...
235
		// Convert PHP errors to exceptions
236
		set_error_handler([$this, 'exceptionErrorHandler'], E_ALL);
237
	}
238
239
	/**
240
	 * Processes PHP errors as exceptions in order to be able to keep track of problems
241
	 *
242
	 * @see https://secure.php.net/manual/en/function.set-error-handler.php
243
	 *
244
	 * @param int $severity the level of the error raised
245
	 * @param string $message
246
	 * @param string $file the filename that the error was raised in
247
	 * @param int $line the line number the error was raised
248
	 *
249
	 * @throws \ErrorException
250
	 */
251
	public function exceptionErrorHandler($severity, $message, $file, $line) {
252
		if (!(error_reporting() & $severity)) {
253
			// This error code is not included in error_reporting
254
			return;
255
		}
256
		throw new \ErrorException($message, 0, $severity, $file, $line);
257
	}
258
259
	/**
260
	 * @param OutputInterface $output
261
	 */
262
	protected function presentStats(OutputInterface $output) {
263
		// Stop the timer
264
		$this->execTime += microtime(true);
265
		$output->writeln("");
266
267
		$headers = [
268
			'Folders', 'Files', 'Elapsed time'
269
		];
270
271
		$this->showSummary($headers, null, $output);
272
	}
273
274
	/**
275
	 * Shows a summary of operations
276
	 *
277
	 * @param string[] $headers
278
	 * @param string[] $rows
279
	 * @param OutputInterface $output
280
	 */
281
	protected function showSummary($headers, $rows, OutputInterface $output) {
282
		$niceDate = $this->formatExecTime();
283
		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...
284
			$rows = [
285
				$this->foldersCounter,
286
				$this->filesCounter,
287
				$niceDate,
288
			];
289
		}
290
		$table = new Table($output);
291
		$table
292
			->setHeaders($headers)
293
			->setRows([$rows]);
294
		$table->render();
295
	}
296
297
298
	/**
299
	 * Formats microtime into a human readable format
300
	 *
301
	 * @return string
302
	 */
303
	protected function formatExecTime() {
304
		list($secs, $tens) = explode('.', sprintf("%.1f", ($this->execTime)));
0 ignored issues
show
Unused Code introduced by
The assignment to $tens is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
305
306
		# if you want to have microseconds add this:   . '.' . $tens;
307
		return date('H:i:s', $secs);
308
	}
309
310
}
311