Passed
Push — master ( eb1755...243bc9 )
by Roeland
10:54 queued 47s
created

Notify::reconnectToDatabase()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 6
nop 2
dl 0
loc 17
rs 9.8333
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016 Robin Appelman <[email protected]>
4
 *
5
 * @author Ari Selseng <[email protected]>
6
 * @author Robin Appelman <[email protected]>
7
 * @author Roeland Jago Douma <[email protected]>
8
 *
9
 * @license GNU AGPL version 3 or any later version
10
 *
11
 * This program is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License as
13
 * published by the Free Software Foundation, either version 3 of the
14
 * License, or (at your option) any later version.
15
 *
16
 * This program is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 * GNU Affero General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU Affero General Public License
22
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23
 *
24
 */
25
26
namespace OCA\Files_External\Command;
27
28
use Doctrine\DBAL\Exception\DriverException;
29
use OC\Core\Command\Base;
30
use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
31
use OCA\Files_External\Lib\StorageConfig;
32
use OCA\Files_External\Service\GlobalStoragesService;
33
use OCP\Files\Notify\IChange;
34
use OCP\Files\Notify\INotifyHandler;
35
use OCP\Files\Notify\IRenameChange;
36
use OCP\Files\Storage\INotifyStorage;
37
use OCP\Files\Storage\IStorage;
38
use OCP\Files\StorageNotAvailableException;
39
use OCP\IDBConnection;
40
use OCP\ILogger;
41
use Symfony\Component\Console\Input\InputArgument;
42
use Symfony\Component\Console\Input\InputInterface;
43
use Symfony\Component\Console\Input\InputOption;
44
use Symfony\Component\Console\Output\OutputInterface;
45
46
class Notify extends Base {
47
	/** @var GlobalStoragesService */
48
	private $globalService;
49
	/** @var IDBConnection */
50
	private $connection;
51
	/** @var \OCP\DB\QueryBuilder\IQueryBuilder */
52
	private $updateQuery;
53
	/** @var ILogger */
54
	private $logger;
55
56
	function __construct(GlobalStoragesService $globalService, IDBConnection $connection, ILogger $logger) {
57
		parent::__construct();
58
		$this->globalService = $globalService;
59
		$this->connection = $connection;
60
		$this->logger = $logger;
61
		$this->updateQuery = $this->getUpdateQuery($this->connection);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getUpdateQuery($this->connection) of type Doctrine\DBAL\Statement is incompatible with the declared type OCP\DB\QueryBuilder\IQueryBuilder of property $updateQuery.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
62
	}
63
64
	protected function configure() {
65
		$this
66
			->setName('files_external:notify')
67
			->setDescription('Listen for active update notifications for a configured external mount')
68
			->addArgument(
69
				'mount_id',
70
				InputArgument::REQUIRED,
71
				'the mount id of the mount to listen to'
72
			)->addOption(
73
				'user',
74
				'u',
75
				InputOption::VALUE_REQUIRED,
76
				'The username for the remote mount (required only for some mount configuration that don\'t store credentials)'
77
			)->addOption(
78
				'password',
79
				'p',
80
				InputOption::VALUE_REQUIRED,
81
				'The password for the remote mount (required only for some mount configuration that don\'t store credentials)'
82
			)->addOption(
83
				'path',
84
				'',
85
				InputOption::VALUE_REQUIRED,
86
				'The directory in the storage to listen for updates in',
87
				'/'
88
			);
89
		parent::configure();
90
	}
91
92
	protected function execute(InputInterface $input, OutputInterface $output) {
93
		$mount = $this->globalService->getStorage($input->getArgument('mount_id'));
94
		if (is_null($mount)) {
95
			$output->writeln('<error>Mount not found</error>');
96
			return 1;
97
		}
98
		$noAuth = false;
99
		try {
100
			$authBackend = $mount->getAuthMechanism();
101
			$authBackend->manipulateStorageConfig($mount);
102
		} catch (InsufficientDataForMeaningfulAnswerException $e) {
103
			$noAuth = true;
104
		} catch (StorageNotAvailableException $e) {
105
			$noAuth = true;
106
		}
107
108
		if ($input->getOption('user')) {
109
			$mount->setBackendOption('user', $input->getOption('user'));
110
		} else if (isset($_ENV['NOTIFY_USER'])) {
111
			$mount->setBackendOption('user', $_ENV['NOTIFY_USER']);
112
		} else if (isset($_SERVER['NOTIFY_USER'])) {
113
			$mount->setBackendOption('user', $_SERVER['NOTIFY_USER']);
114
		}
115
		if ($input->getOption('password')) {
116
			$mount->setBackendOption('password', $input->getOption('password'));
117
		} else if (isset($_ENV['NOTIFY_PASSWORD'])) {
118
			$mount->setBackendOption('password', $_ENV['NOTIFY_PASSWORD']);
119
		} else if (isset($_SERVER['NOTIFY_PASSWORD'])) {
120
			$mount->setBackendOption('password', $_SERVER['NOTIFY_PASSWORD']);
121
		}
122
123
		try {
124
			$storage = $this->createStorage($mount);
125
		} catch (\Exception $e) {
126
			$output->writeln('<error>Error while trying to create storage</error>');
127
			if ($noAuth) {
128
				$output->writeln('<error>Username and/or password required</error>');
129
			}
130
			return 1;
131
		}
132
		if (!$storage instanceof INotifyStorage) {
133
			$output->writeln('<error>Mount of type "' . $mount->getBackend()->getText() . '" does not support active update notifications</error>');
134
			return 1;
135
		}
136
137
		$verbose = $input->getOption('verbose');
138
139
		$path = trim($input->getOption('path'), '/');
140
		$notifyHandler = $storage->notify($path);
141
		$this->selfTest($storage, $notifyHandler, $verbose, $output);
142
		$notifyHandler->listen(function (IChange $change) use ($mount, $verbose, $output) {
143
			if ($verbose) {
144
				$this->logUpdate($change, $output);
145
			}
146
			if ($change instanceof IRenameChange) {
147
				$this->markParentAsOutdated($mount->getId(), $change->getTargetPath(), $output);
148
			}
149
			$this->markParentAsOutdated($mount->getId(), $change->getPath(), $output);
150
		});
151
	}
152
153
	private function createStorage(StorageConfig $mount) {
154
		$class = $mount->getBackend()->getStorageClass();
155
		return new $class($mount->getBackendOptions());
156
	}
157
158
	private function markParentAsOutdated($mountId, $path, OutputInterface $output) {
159
		$parent = ltrim(dirname($path), '/');
160
		if ($parent === '.') {
161
			$parent = '';
162
		}
163
164
		try {
165
			$this->updateQuery->execute([$parent, $mountId]);
0 ignored issues
show
Unused Code introduced by
The call to OCP\DB\QueryBuilder\IQueryBuilder::execute() has too many arguments starting with array($parent, $mountId). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

165
			$this->updateQuery->/** @scrutinizer ignore-call */ 
166
                       execute([$parent, $mountId]);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
166
		} catch (DriverException $ex) {
167
			$this->logger->logException($ex, ['app' => 'files_external', 'message' => 'Error while trying to mark folder as outdated', 'level' => ILogger::WARN]);
168
			$this->connection = $this->reconnectToDatabase($this->connection, $output);
169
			$output->writeln('<info>Needed to reconnect to the database</info>');
170
			$this->updateQuery = $this->getUpdateQuery($this->connection);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getUpdateQuery($this->connection) of type Doctrine\DBAL\Statement is incompatible with the declared type OCP\DB\QueryBuilder\IQueryBuilder of property $updateQuery.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
171
			$this->updateQuery->execute([$parent, $mountId]);
172
		}
173
	}
174
175
	private function logUpdate(IChange $change, OutputInterface $output) {
176
		switch ($change->getType()) {
177
			case INotifyStorage::NOTIFY_ADDED:
178
				$text = 'added';
179
				break;
180
			case INotifyStorage::NOTIFY_MODIFIED:
181
				$text = 'modified';
182
				break;
183
			case INotifyStorage::NOTIFY_REMOVED:
184
				$text = 'removed';
185
				break;
186
			case INotifyStorage::NOTIFY_RENAMED:
187
				$text = 'renamed';
188
				break;
189
			default:
190
				return;
191
		}
192
193
		$text .= ' ' . $change->getPath();
194
		if ($change instanceof IRenameChange) {
195
			$text .= ' to ' . $change->getTargetPath();
196
		}
197
198
		$output->writeln($text);
199
	}
200
201
	/**
202
	 * @return \Doctrine\DBAL\Statement
203
	*/
204
	private function getUpdateQuery(IDBConnection $connection) {
205
		// the query builder doesn't really like subqueries with parameters
206
		return $connection->prepare(
207
			'UPDATE *PREFIX*filecache SET size = -1
208
			WHERE `path` = ?
209
			AND `storage` IN (SELECT storage_id FROM *PREFIX*mounts WHERE mount_id = ?)'
210
		);
211
	}
212
213
	/**
214
	 * @return \OCP\IDBConnection
215
	*/
216
	private function reconnectToDatabase(IDBConnection $connection, OutputInterface $output) {
217
		try {
218
			$connection->close();
219
		} catch (\Exception $ex) {
220
			$this->logger->logException($ex, ['app' => 'files_external', 'message' => 'Error while disconnecting from DB', 'level' => ILogger::WARN]);
221
			$output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
222
		}
223
		while (!$connection->isConnected()) {
0 ignored issues
show
Bug introduced by
The method isConnected() does not exist on OCP\IDBConnection. Since it exists in all sub-types, consider adding an abstract or default implementation to OCP\IDBConnection. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

223
		while (!$connection->/** @scrutinizer ignore-call */ isConnected()) {
Loading history...
224
			try {
225
				$connection->connect();
226
			} catch (\Exception $ex) {
227
				$this->logger->logException($ex, ['app' => 'files_external', 'message' => 'Error while re-connecting to database', 'level' => ILogger::WARN]);
228
				$output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
229
				sleep(60);
230
			}
231
		}
232
		return $connection;
233
	}
234
235
236
	private function selfTest(IStorage $storage, INotifyHandler $notifyHandler, $verbose, OutputInterface $output) {
237
		usleep(100 * 1000); //give time for the notify to start
238
		$storage->file_put_contents('/.nc_test_file.txt', 'test content');
239
		$storage->mkdir('/.nc_test_folder');
240
		$storage->file_put_contents('/.nc_test_folder/subfile.txt', 'test content');
241
242
		usleep(100 * 1000); //time for all changes to be processed
243
		$changes = $notifyHandler->getChanges();
244
245
		$storage->unlink('/.nc_test_file.txt');
246
		$storage->unlink('/.nc_test_folder/subfile.txt');
247
		$storage->rmdir('/.nc_test_folder');
248
249
		usleep(100 * 1000); //time for all changes to be processed
250
		$notifyHandler->getChanges(); // flush
251
252
		$foundRootChange = false;
253
		$foundSubfolderChange = false;
254
255
		foreach ($changes as $change) {
256
			if ($change->getPath() === '/.nc_test_file.txt' || $change->getPath() === '.nc_test_file.txt') {
257
				$foundRootChange = true;
258
			} else if ($change->getPath() === '/.nc_test_folder/subfile.txt' || $change->getPath() === '.nc_test_folder/subfile.txt') {
259
				$foundSubfolderChange = true;
260
			}
261
		}
262
263
		if ($foundRootChange && $foundSubfolderChange && $verbose) {
264
			$output->writeln('<info>Self-test successful</info>');
265
		} else if ($foundRootChange && !$foundSubfolderChange) {
266
			$output->writeln('<error>Error while running self-test, change is subfolder not detected</error>');
267
		} else if (!$foundRootChange) {
268
			$output->writeln('<error>Error while running self-test, no changes detected</error>');
269
		}
270
	}
271
}
272