Completed
Push — stable10 ( 0a1b1d...dbc860 )
by Lukas
205:07 queued 195:01
created

Scan::reconnectToDatabase()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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