Completed
Push — master ( 69e92e...e4c3c4 )
by Morris
169:48 queued 151:27
created

Scan::filterHomeMount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
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 View Code Duplication
		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
				if ($this->hasBeenInterrupted()) {
133
					throw new InterruptedException();
134
				}
135
			});
136
			$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
137
				$output->writeln("\tFolder <info>$path</info>");
138
				$this->foldersCounter += 1;
139
				if ($this->hasBeenInterrupted()) {
140
					throw new InterruptedException();
141
				}
142
			});
143
			$scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) {
144
				$output->writeln("Error while scanning, storage not available (" . $e->getMessage() . ")");
145
			});
146
			# count only
147
		} else {
148
			$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function () use ($output) {
149
				$this->filesCounter += 1;
150
				if ($this->hasBeenInterrupted()) {
151
					throw new InterruptedException();
152
				}
153
			});
154
			$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function () use ($output) {
155
				$this->foldersCounter += 1;
156
				if ($this->hasBeenInterrupted()) {
157
					throw new InterruptedException();
158
				}
159
			});
160
		}
161
		$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) {
162
			$this->checkScanWarning($path, $output);
163
		});
164
		$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
165
			$this->checkScanWarning($path, $output);
166
		});
167
168
		try {
169
			if ($backgroundScan) {
170
				$scanner->backgroundScan($path);
171
			} else {
172
				$scanner->scan($path, $recursive, $homeOnly ? [$this, 'filterHomeMount'] : null);
173
			}
174
		} catch (ForbiddenException $e) {
175
			$output->writeln("<error>Home storage for user $user not writable</error>");
176
			$output->writeln("Make sure you're running the scan command only as the user the web server runs as");
177
		} catch (InterruptedException $e) {
178
			# exit the function if ctrl-c has been pressed
179
			$output->writeln('Interrupted by user');
180
		} catch (NotFoundException $e) {
181
			$output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>');
182
		} catch (\Exception $e) {
183
			$output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>');
184
			$output->writeln('<error>' . $e->getTraceAsString() . '</error>');
185
		}
186
	}
187
188
	public function filterHomeMount(IMountPoint $mountPoint) {
189
		// any mountpoint inside '/$user/files/'
190
		return substr_count($mountPoint->getMountPoint(), '/') <= 3;
191
	}
192
193
	protected function execute(InputInterface $input, OutputInterface $output) {
194
		$inputPath = $input->getOption('path');
195
		if ($inputPath) {
196
			$inputPath = '/' . trim($inputPath, '/');
197
			list (, $user,) = explode('/', $inputPath, 3);
198
			$users = array($user);
199
		} else if ($input->getOption('all')) {
200
			$users = $this->userManager->search('');
201
		} else {
202
			$users = $input->getArgument('user_id');
203
		}
204
205
		# no messaging level option means: no full printout but statistics
206
		# $quiet   means no print at all
207
		# $verbose means full printout including statistics
208
		# -q	-v	full	stat
209
		#  0	 0	no	yes
210
		#  0	 1	yes	yes
211
		#  1	--	no	no  (quiet overrules verbose)
212
		$verbose = $input->getOption('verbose');
213
		$quiet = $input->getOption('quiet');
214
		# restrict the verbosity level to VERBOSITY_VERBOSE
215
		if ($output->getVerbosity() > OutputInterface::VERBOSITY_VERBOSE) {
216
			$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
217
		}
218
		if ($quiet) {
219
			$verbose = false;
220
		}
221
222
		# check quantity of users to be process and show it on the command line
223
		$users_total = count($users);
224
		if ($users_total === 0) {
225
			$output->writeln("<error>Please specify the user id to scan, \"--all\" to scan for all users or \"--path=...\"</error>");
226
			return;
227
		} else {
228
			if ($users_total > 1) {
229
				$output->writeln("\nScanning files for $users_total users");
230
			}
231
		}
232
233
		$this->initTools();
234
235
		$user_count = 0;
236
		foreach ($users as $user) {
237
			if (is_object($user)) {
238
				$user = $user->getUID();
239
			}
240
			$path = $inputPath ? $inputPath : '/' . $user;
241
			$user_count += 1;
242
			if ($this->userManager->userExists($user)) {
243
				# add an extra line when verbose is set to optical separate users
244
				if ($verbose) {
245
					$output->writeln("");
246
				}
247
				$output->writeln("Starting scan for user $user_count out of $users_total ($user)");
248
				# full: printout data if $verbose was set
249
				$this->scanFiles($user, $path, $verbose, $output, $input->getOption('unscanned'), ! $input->getOption('shallow'), $input->getOption('home-only'));
250
			} else {
251
				$output->writeln("<error>Unknown user $user_count $user</error>");
252
			}
253
			# check on each user if there was a user interrupt (ctrl-c) and exit foreach
254
			if ($this->hasBeenInterrupted()) {
255
				break;
256
			}
257
		}
258
259
		# stat: printout statistics if $quiet was not set
260
		if (!$quiet) {
261
			$this->presentStats($output);
262
		}
263
	}
264
265
	/**
266
	 * Initialises some useful tools for the Command
267
	 */
268
	protected function initTools() {
269
		// Start the timer
270
		$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...
271
		// Convert PHP errors to exceptions
272
		set_error_handler([$this, 'exceptionErrorHandler'], E_ALL);
273
	}
274
275
	/**
276
	 * Processes PHP errors as exceptions in order to be able to keep track of problems
277
	 *
278
	 * @see https://secure.php.net/manual/en/function.set-error-handler.php
279
	 *
280
	 * @param int $severity the level of the error raised
281
	 * @param string $message
282
	 * @param string $file the filename that the error was raised in
283
	 * @param int $line the line number the error was raised
284
	 *
285
	 * @throws \ErrorException
286
	 */
287
	public function exceptionErrorHandler($severity, $message, $file, $line) {
288
		if (!(error_reporting() & $severity)) {
289
			// This error code is not included in error_reporting
290
			return;
291
		}
292
		throw new \ErrorException($message, 0, $severity, $file, $line);
293
	}
294
295
	/**
296
	 * @param OutputInterface $output
297
	 */
298 View Code Duplication
	protected function presentStats(OutputInterface $output) {
299
		// Stop the timer
300
		$this->execTime += microtime(true);
301
		$output->writeln("");
302
303
		$headers = [
304
			'Folders', 'Files', 'Elapsed time'
305
		];
306
307
		$this->showSummary($headers, null, $output);
308
	}
309
310
	/**
311
	 * Shows a summary of operations
312
	 *
313
	 * @param string[] $headers
314
	 * @param string[] $rows
315
	 * @param OutputInterface $output
316
	 */
317 View Code Duplication
	protected function showSummary($headers, $rows, OutputInterface $output) {
318
		$niceDate = $this->formatExecTime();
319
		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...
320
			$rows = [
321
				$this->foldersCounter,
322
				$this->filesCounter,
323
				$niceDate,
324
			];
325
		}
326
		$table = new Table($output);
327
		$table
328
			->setHeaders($headers)
329
			->setRows([$rows]);
330
		$table->render();
331
	}
332
333
334
	/**
335
	 * Formats microtime into a human readable format
336
	 *
337
	 * @return string
338
	 */
339
	protected function formatExecTime() {
340
		list($secs, ) = explode('.', sprintf("%.1f", $this->execTime));
341
342
		# if you want to have microseconds add this:   . '.' . $tens;
343
		return date('H:i:s', $secs);
344
	}
345
346
	/**
347
	 * @return \OCP\IDBConnection
348
	 */
349 View Code Duplication
	protected function reconnectToDatabase(OutputInterface $output) {
350
		/** @var Connection | IDBConnection $connection */
351
		$connection = \OC::$server->getDatabaseConnection();
352
		try {
353
			$connection->close();
354
		} catch (\Exception $ex) {
355
			$output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
356
		}
357
		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...
358
			try {
359
				$connection->connect();
360
			} catch (\Exception $ex) {
361
				$output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
362
				sleep(60);
363
			}
364
		}
365
		return $connection;
366
	}
367
368
}
369