Completed
Pull Request — stable9 (#1149)
by Lukas
246:54 queued 237:57
created

Scan::scanFiles()   C

Complexity

Conditions 8
Paths 6

Size

Total Lines 49
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

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