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

ScanAppData   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 257
Duplicated Lines 20.23 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
dl 52
loc 257
rs 10
c 0
b 0
f 0
wmc 28
lcom 1
cbo 11

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A configure() 0 19 1
A checkScanWarning() 8 8 2
B scanFiles() 0 59 7
A execute() 0 29 4
A initTools() 0 6 1
A exceptionErrorHandler() 0 7 2
A presentStats() 11 11 1
A showSummary() 15 15 2
A formatExecTime() 0 6 1
A reconnectToDatabase() 18 18 4
A getAppDataFolder() 0 9 2

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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